ace-lab 0.8.0 → 0.13.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 +363 -92
- data/README.md +66 -100
- data/docs/usage.md +529 -404
- data/lib/ace/lab/atoms/install_schema.rb +373 -0
- data/lib/ace/lab/cli/commands/install.rb +161 -0
- data/lib/ace/lab/cli/commands/prepare.rb +118 -0
- data/lib/ace/lab/cli/commands/project_add.rb +101 -0
- data/lib/ace/lab/cli/commands/status.rb +109 -0
- data/lib/ace/lab/cli/commands/verify.rb +115 -0
- data/lib/ace/lab/cli.rb +21 -13
- data/lib/ace/lab/install_manifest_path.rb +23 -0
- data/lib/ace/lab/models/status_report.rb +38 -0
- data/lib/ace/lab/molecules/account_provisioner.rb +196 -0
- data/lib/ace/lab/molecules/config_state_classifier.rb +145 -0
- data/lib/ace/lab/molecules/directory_provisioner.rb +208 -0
- data/lib/ace/lab/molecules/grant_provisioner.rb +250 -0
- data/lib/ace/lab/molecules/grant_resolver.rb +49 -23
- data/lib/ace/lab/molecules/hitl_routes.rb +263 -0
- data/lib/ace/lab/molecules/install_manifest_loader.rb +64 -0
- data/lib/ace/lab/molecules/key_provisioner.rb +736 -0
- data/lib/ace/lab/molecules/lab_state_scanner.rb +55 -0
- data/lib/ace/lab/molecules/overseer_provisioner.rb +188 -0
- data/lib/ace/lab/molecules/project_registry.rb +251 -0
- data/lib/ace/lab/molecules/repository_provisioner.rb +141 -0
- data/lib/ace/lab/molecules/role_verifier.rb +223 -0
- data/lib/ace/lab/organisms/install.rb +213 -0
- data/lib/ace/lab/organisms/prepare.rb +156 -0
- data/lib/ace/lab/organisms/project_add.rb +170 -0
- data/lib/ace/lab/organisms/status_service.rb +70 -0
- data/lib/ace/lab/organisms/verify.rb +768 -0
- data/lib/ace/lab/version.rb +1 -1
- data/lib/ace/lab.rb +34 -6
- metadata +34 -102
- data/lib/ace/lab/atoms/protected_publication_input.rb +0 -50
- data/lib/ace/lab/atoms/protected_workspace_prune_input.rb +0 -114
- data/lib/ace/lab/atoms/protected_workspace_prune_preview.rb +0 -86
- data/lib/ace/lab/atoms/service_input.rb +0 -92
- data/lib/ace/lab/cli/commands/assignment.rb +0 -45
- data/lib/ace/lab/cli/commands/service.rb +0 -240
- data/lib/ace/lab/models/service_receipt.rb +0 -18
- data/lib/ace/lab/molecules/hitl_authorizer.rb +0 -63
- data/lib/ace/lab/molecules/ordinary_admission_resolver.rb +0 -131
- data/lib/ace/lab/molecules/ordinary_listener.rb +0 -262
- data/lib/ace/lab/molecules/ordinary_merge_read.rb +0 -45
- data/lib/ace/lab/molecules/ordinary_native_read.rb +0 -106
- data/lib/ace/lab/molecules/ordinary_owner_admission.rb +0 -139
- data/lib/ace/lab/molecules/ordinary_owner_config.rb +0 -232
- data/lib/ace/lab/molecules/ordinary_receiver_config.rb +0 -94
- data/lib/ace/lab/molecules/ordinary_socket.rb +0 -126
- data/lib/ace/lab/molecules/ordinary_worktree_read.rb +0 -216
- data/lib/ace/lab/molecules/protected_cleanup_owner_admission.rb +0 -163
- data/lib/ace/lab/molecules/protected_cleanup_owner_client.rb +0 -165
- data/lib/ace/lab/molecules/protected_cleanup_owner_identity.rb +0 -239
- data/lib/ace/lab/molecules/protected_publication_publisher.rb +0 -110
- data/lib/ace/lab/molecules/protected_service_handler.rb +0 -43
- data/lib/ace/lab/molecules/protected_service_policy.rb +0 -156
- data/lib/ace/lab/molecules/service_executor.rb +0 -99
- data/lib/ace/lab/molecules/service_policy.rb +0 -289
- data/lib/ace/lab/organisms/authority_composition.rb +0 -87
- data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -149
- data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -123
- data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -688
- data/lib/ace/lab/organisms/ordinary_merge_request.rb +0 -456
- data/lib/ace/lab/organisms/protected_cleanup_owner.rb +0 -361
- data/lib/ace/lab/organisms/protected_service_client.rb +0 -129
- data/lib/ace/lab/organisms/protected_service_listener.rb +0 -181
- data/lib/ace/lab/organisms/protected_service_publication.rb +0 -268
- data/lib/ace/lab/organisms/protected_service_receiver.rb +0 -267
- data/lib/ace/lab/organisms/protected_service_recovery.rb +0 -185
- data/lib/ace/lab/organisms/protected_service_request.rb +0 -128
- data/lib/ace/lab/organisms/protected_service_worker.rb +0 -89
- data/lib/ace/lab/organisms/service_request_service.rb +0 -249
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../errors"
|
|
4
|
-
require_relative "../atoms/service_input"
|
|
5
|
-
require "ace/assign/errors"
|
|
6
|
-
require "pathname"
|
|
7
|
-
require "time"
|
|
8
|
-
|
|
9
|
-
module Ace
|
|
10
|
-
module Lab
|
|
11
|
-
module Molecules
|
|
12
|
-
# Deployment-owned operation and authorization facts. The caller may
|
|
13
|
-
# name a reference but cannot define its scope or executor binding.
|
|
14
|
-
class ServicePolicy
|
|
15
|
-
NAME = /\A[a-z][a-z0-9-]{0,63}\z/
|
|
16
|
-
ID = /\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/
|
|
17
|
-
|
|
18
|
-
def initialize(document, proposal_resolver = nil)
|
|
19
|
-
@document = document
|
|
20
|
-
@operations = document.fetch("operations", {})
|
|
21
|
-
@authorizations = document.fetch("authorizations", {})
|
|
22
|
-
@proposal_resolver = proposal_resolver
|
|
23
|
-
unless @operations.is_a?(Hash) && @authorizations.is_a?(Hash)
|
|
24
|
-
raise Ace::Lab::InvalidConfigurationError, "trusted service policy must contain mappings"
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# The trusted installation document as loaded. Decisions are mutable;
|
|
29
|
-
# all other configuration remains bound for the receiver lifetime.
|
|
30
|
-
attr_reader :document
|
|
31
|
-
|
|
32
|
-
def self.configuration_digest(document)
|
|
33
|
-
Atoms::ServiceInput.digest(document.except("authorizations"))
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def operation!(name, project:, service_id:)
|
|
37
|
-
raise ArgumentError, "invalid operation name" unless name.to_s.match?(NAME)
|
|
38
|
-
operation = @operations[name]
|
|
39
|
-
raise SecurityError, "operation is not configured" unless operation.is_a?(Hash)
|
|
40
|
-
unless operation["project"] == project && operation["service_id"] == service_id
|
|
41
|
-
raise SecurityError, "operation does not authorize the selected service in this project"
|
|
42
|
-
end
|
|
43
|
-
transport = operation.fetch("transport", "local")
|
|
44
|
-
unless %w[local unix].include?(transport)
|
|
45
|
-
raise Ace::Lab::InvalidConfigurationError, "configured service transport is invalid"
|
|
46
|
-
end
|
|
47
|
-
if transport == "local"
|
|
48
|
-
argv = operation["argv"]
|
|
49
|
-
unless argv.is_a?(Array) && !argv.empty? && argv.all? { |v| v.is_a?(String) && !v.empty? } &&
|
|
50
|
-
argv.first.start_with?("/") && File.file?(argv.first) && File.executable?(argv.first)
|
|
51
|
-
raise Ace::Lab::InvalidConfigurationError, "configured operation needs a fixed executable argv"
|
|
52
|
-
end
|
|
53
|
-
elsif !operation["socket_path"].is_a?(String) || !operation["socket_path"].start_with?("/")
|
|
54
|
-
raise Ace::Lab::InvalidConfigurationError, "unix service needs an absolute socket path"
|
|
55
|
-
end
|
|
56
|
-
uid = operation["executor_uid"]
|
|
57
|
-
raise Ace::Lab::InvalidConfigurationError, "configured executor UID is invalid" unless uid.is_a?(Integer) && uid >= 0
|
|
58
|
-
if transport == "local"
|
|
59
|
-
# Every path component is validated (symlinks resolved, writable
|
|
60
|
-
# ancestors rejected) and the validated resolved file replaces
|
|
61
|
-
# the configured argv entry, so execution cannot be redirected
|
|
62
|
-
# between validation and dispatch.
|
|
63
|
-
resolved = trusted_executable!(operation["argv"].first, uid)
|
|
64
|
-
operation = operation.merge("argv" => [resolved] + operation["argv"][1..])
|
|
65
|
-
end
|
|
66
|
-
expires_at = parse_time(operation["lease_expires_at"])
|
|
67
|
-
raise SecurityError, "executor lease has expired" if expires_at <= Time.now.utc
|
|
68
|
-
if operation["host_maintenance"] == true
|
|
69
|
-
# Maintenance executes a fixed updater outside the replaced
|
|
70
|
-
# deployment; a socket-only operation has no executable to place
|
|
71
|
-
# and cannot prove the maintenance boundary.
|
|
72
|
-
unless transport == "local"
|
|
73
|
-
raise Ace::Lab::InvalidConfigurationError,
|
|
74
|
-
"host maintenance requires a local executable transport"
|
|
75
|
-
end
|
|
76
|
-
deployment_root = operation["deployment_root"].to_s
|
|
77
|
-
sink_path = operation["evidence_sink"].to_s
|
|
78
|
-
unless deployment_root.start_with?("/") && sink_path.start_with?("/")
|
|
79
|
-
raise Ace::Lab::InvalidConfigurationError,
|
|
80
|
-
"host maintenance needs absolute deployment and evidence sink paths"
|
|
81
|
-
end
|
|
82
|
-
deployment = resolved_path(deployment_root)
|
|
83
|
-
sink = resolved_path(sink_path)
|
|
84
|
-
executable = operation.dig("argv", 0).to_s
|
|
85
|
-
unless deployment.start_with?("/") && sink.start_with?("/") && executable.start_with?("/") &&
|
|
86
|
-
!inside?(executable, deployment) && !inside?(sink, deployment)
|
|
87
|
-
raise Ace::Lab::InvalidConfigurationError,
|
|
88
|
-
"host maintenance needs executable and evidence sink outside the replaced deployment"
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
operation
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def authorize!(reference, binding)
|
|
95
|
-
raise ArgumentError, "invalid authorization reference" unless reference.to_s.match?(ID)
|
|
96
|
-
if reference.start_with?("proposal-")
|
|
97
|
-
raise SecurityError, "canonical proposal authority is unavailable" unless @proposal_resolver
|
|
98
|
-
begin
|
|
99
|
-
return @proposal_resolver.call(reference, binding)
|
|
100
|
-
rescue Ace::Assign::Error
|
|
101
|
-
raise SecurityError, "canonical proposal does not authorize this exact effect"
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
decision = @authorizations[reference]
|
|
105
|
-
raise SecurityError, "authorization reference is unresolved" unless decision.is_a?(Hash)
|
|
106
|
-
required = %w[operation project_id assignment_id attempt_id input_digest target candidate_head caller_uid]
|
|
107
|
-
unless required.all? { |key| decision[key] == binding[key] }
|
|
108
|
-
raise SecurityError, "authorization does not match the exact service request"
|
|
109
|
-
end
|
|
110
|
-
raise SecurityError, "authorization has expired" if parse_time(decision["expires_at"]) <= Time.now.utc
|
|
111
|
-
decision
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# ---- ordinary fixed merge admission policy (spec 8x3.t.xz9.4) ------
|
|
115
|
-
#
|
|
116
|
-
# The installed ordinary section is the separately configured
|
|
117
|
-
# ordinary receiver mapping and admission policy. It supplies
|
|
118
|
-
# identity and placement only: the caller selects no endpoint,
|
|
119
|
-
# credential or receiver. An absent project section or a disabled
|
|
120
|
-
# admission flag is rollback: new admission is unavailable while
|
|
121
|
-
# canonical records and read-only status stay preserved.
|
|
122
|
-
ORDINARY_SECTION_KEYS = %w[projects].freeze
|
|
123
|
-
ORDINARY_PROJECT_KEYS = %w[receiver_id socket_path receiver_uid principal admission journal_repository journal_ref].freeze
|
|
124
|
-
ORDINARY_AUTHORIZATION_KEYS = %w[operation project_id assignment_id attempt_id queue_entry
|
|
125
|
-
input_digest forge pr_url head_repository head_ref candidate_head base_repository
|
|
126
|
-
base_ref expected_base_head expires_at].freeze
|
|
127
|
-
# expires_at bounds the decision itself; it is never exact-matched
|
|
128
|
-
# against the request binding, which carries no expiry.
|
|
129
|
-
ORDINARY_BINDING_KEYS = ORDINARY_AUTHORIZATION_KEYS - %w[expires_at].freeze
|
|
130
|
-
|
|
131
|
-
def ordinary_receiver!(project)
|
|
132
|
-
section = @document["ordinary"]
|
|
133
|
-
return nil unless section.is_a?(Hash)
|
|
134
|
-
validate_ordinary_section!(section)
|
|
135
|
-
entry = section.fetch("projects")[project.to_s]
|
|
136
|
-
return nil unless entry.is_a?(Hash)
|
|
137
|
-
entry
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# The ordinary admission decision: exact-match on every closed
|
|
141
|
-
# binding key, like authorize!, but over the ordinary request
|
|
142
|
-
# identity (forge/PR/repository/ref) instead of execution selectors.
|
|
143
|
-
def authorize_ordinary!(reference, binding)
|
|
144
|
-
raise ArgumentError, "invalid authorization reference" unless reference.to_s.match?(ID)
|
|
145
|
-
decision = @authorizations[reference]
|
|
146
|
-
raise SecurityError, "authorization reference is unresolved" unless decision.is_a?(Hash)
|
|
147
|
-
unless ORDINARY_AUTHORIZATION_KEYS.all? { |key| decision.key?(key) }
|
|
148
|
-
raise SecurityError, "authorization reference is not an ordinary merge decision"
|
|
149
|
-
end
|
|
150
|
-
unless ORDINARY_BINDING_KEYS.all? { |key| decision[key] == binding[key] }
|
|
151
|
-
raise SecurityError, "authorization does not match the exact ordinary merge request"
|
|
152
|
-
end
|
|
153
|
-
raise SecurityError, "authorization has expired" if parse_time(decision["expires_at"]) <= Time.now.utc
|
|
154
|
-
decision
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def validate_ordinary_section!(section)
|
|
158
|
-
unless (section.keys - ORDINARY_SECTION_KEYS).empty? && section["projects"].is_a?(Hash)
|
|
159
|
-
raise Ace::Lab::InvalidConfigurationError, "ordinary section must map projects"
|
|
160
|
-
end
|
|
161
|
-
section.fetch("projects").each do |project, entry|
|
|
162
|
-
unless project.is_a?(String) && project.match?(ID) && entry.is_a?(Hash) &&
|
|
163
|
-
(entry.keys - ORDINARY_PROJECT_KEYS).empty?
|
|
164
|
-
raise Ace::Lab::InvalidConfigurationError, "ordinary project section is invalid"
|
|
165
|
-
end
|
|
166
|
-
unless entry["receiver_id"].is_a?(String) && entry["receiver_id"].match?(ID) &&
|
|
167
|
-
entry["socket_path"].is_a?(String) && entry["socket_path"].start_with?("/") &&
|
|
168
|
-
entry["receiver_uid"].is_a?(Integer) && entry["receiver_uid"].positive? &&
|
|
169
|
-
entry["principal"].is_a?(String) && !entry["principal"].empty?
|
|
170
|
-
raise Ace::Lab::InvalidConfigurationError, "ordinary receiver mapping is incomplete"
|
|
171
|
-
end
|
|
172
|
-
admission = entry.fetch("admission", "enabled")
|
|
173
|
-
unless %w[enabled disabled].include?(admission)
|
|
174
|
-
raise Ace::Lab::InvalidConfigurationError, "ordinary admission flag is invalid"
|
|
175
|
-
end
|
|
176
|
-
repository, ref = entry.values_at("journal_repository", "journal_ref")
|
|
177
|
-
unless repository.is_a?(String) && repository.start_with?("/") && !repository.include?("\0") &&
|
|
178
|
-
File.expand_path(repository) == repository &&
|
|
179
|
-
ref.is_a?(String) && ref.match?(%r{\Arefs/[a-zA-Z0-9_-]+(?:/[a-zA-Z0-9_-]+)+\z})
|
|
180
|
-
raise Ace::Lab::InvalidConfigurationError, "ordinary journal selection is invalid"
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
true
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# Inspection never renews effect permission or selects effect argv.
|
|
187
|
-
def inspection!(name, project:, service_id:, executor_uid:)
|
|
188
|
-
raise ArgumentError, "invalid inspection operation" unless name.is_a?(String) && name.match?(NAME)
|
|
189
|
-
operation = @operations[name]
|
|
190
|
-
unless operation.is_a?(Hash) && operation["project"] == project && operation["service_id"] == service_id &&
|
|
191
|
-
operation["executor_uid"].is_a?(Integer) && operation["executor_uid"] == executor_uid &&
|
|
192
|
-
operation.fetch("transport", "local") == "local"
|
|
193
|
-
raise SecurityError, "inspection does not match the selected original executor"
|
|
194
|
-
end
|
|
195
|
-
# This source-composed inspector uses the fixed original root SDK;
|
|
196
|
-
# configured handler argv cannot select or replace that owner.
|
|
197
|
-
if name == "prune-preserved-workspace"
|
|
198
|
-
return {"executor_uid" => executor_uid, "operation" => name}
|
|
199
|
-
end
|
|
200
|
-
argv = operation["no_effect_argv"]
|
|
201
|
-
unless argv.is_a?(Array) && argv.length.between?(1, 16) &&
|
|
202
|
-
argv.all? { |value| value.is_a?(String) && value.bytesize.between?(1, 1024) && !value.include?("\0") } &&
|
|
203
|
-
argv.first.start_with?("/")
|
|
204
|
-
raise Ace::Lab::InvalidConfigurationError, "operation has no fixed no-effect inspector"
|
|
205
|
-
end
|
|
206
|
-
executable = trusted_executable!(argv.first, executor_uid)
|
|
207
|
-
{"executor_uid" => executor_uid, "argv" => [executable] + argv.drop(1)}
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
private
|
|
211
|
-
|
|
212
|
-
# Validate the executable and every directory component on its
|
|
213
|
-
# resolved path: each must be owned by root or the executor and not
|
|
214
|
-
# group/other-writable, so no ancestor or symlink component can
|
|
215
|
-
# redirect the fixed argv. Returns the resolved file path.
|
|
216
|
-
def trusted_executable!(path, uid)
|
|
217
|
-
resolved = resolved_path(path.to_s)
|
|
218
|
-
unless resolved.start_with?("/") && File.file?(resolved) && File.executable?(resolved)
|
|
219
|
-
raise Ace::Lab::InvalidConfigurationError, "configured executable is unavailable"
|
|
220
|
-
end
|
|
221
|
-
Pathname.new(resolved).descend do |component|
|
|
222
|
-
stat = File.stat(component)
|
|
223
|
-
unless [0, uid].include?(stat.uid) && (stat.mode & 0o022).zero?
|
|
224
|
-
raise Ace::Lab::InvalidConfigurationError,
|
|
225
|
-
"configured executable path has a writable or foreign-owned component: #{component}"
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
|
-
resolved
|
|
229
|
-
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
230
|
-
raise Ace::Lab::InvalidConfigurationError, "configured executable is unavailable"
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def inside?(path, root)
|
|
234
|
-
expanded = File.expand_path(path)
|
|
235
|
-
base = File.expand_path(root)
|
|
236
|
-
expanded == base || expanded.start_with?(base + File::SEPARATOR)
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# Resolve a configured path through symlinks so placement checks see
|
|
240
|
-
# the real location. Paths may not exist yet (an evidence sink is
|
|
241
|
-
# created on first use), so the deepest ancestor — counting symlink
|
|
242
|
-
# components themselves via lstat — is resolved instead of failing;
|
|
243
|
-
# a dangling symlink resolves through its own target chain rather
|
|
244
|
-
# than hiding behind its parent.
|
|
245
|
-
MAX_SYMLINK_DEPTH = 16
|
|
246
|
-
|
|
247
|
-
def resolved_path(path, depth = 0)
|
|
248
|
-
raise Ace::Lab::InvalidConfigurationError, "trusted policy path has a symlink cycle" if depth > MAX_SYMLINK_DEPTH
|
|
249
|
-
expanded = File.expand_path(path)
|
|
250
|
-
candidate = expanded
|
|
251
|
-
candidate = File.dirname(candidate) until candidate == "/" || lstat?(candidate)
|
|
252
|
-
resolved = begin
|
|
253
|
-
File.realpath(candidate)
|
|
254
|
-
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
255
|
-
dangling_target(candidate, depth) || candidate
|
|
256
|
-
end
|
|
257
|
-
return resolved unless resolved == candidate && lstat?(candidate) && File.symlink?(candidate)
|
|
258
|
-
dangling_target(candidate, depth) || resolved
|
|
259
|
-
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
260
|
-
expanded
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
# Follow a dangling symlink's own target chain; nil when the link
|
|
264
|
-
# cannot be resolved to any concrete location.
|
|
265
|
-
def dangling_target(link, depth = 0)
|
|
266
|
-
raise Ace::Lab::InvalidConfigurationError, "trusted policy path has a symlink cycle" if depth > MAX_SYMLINK_DEPTH
|
|
267
|
-
target = File.readlink(link)
|
|
268
|
-
target = File.expand_path(target, File.dirname(link)) unless Pathname.new(target).absolute?
|
|
269
|
-
resolved_path(target, depth + 1)
|
|
270
|
-
rescue Errno::ENOENT, Errno::EACCES
|
|
271
|
-
nil
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
def lstat?(path)
|
|
275
|
-
File.lstat(path)
|
|
276
|
-
true
|
|
277
|
-
rescue Errno::ENOENT
|
|
278
|
-
false
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def parse_time(value)
|
|
282
|
-
Time.iso8601(value.to_s)
|
|
283
|
-
rescue ArgumentError
|
|
284
|
-
raise Ace::Lab::InvalidConfigurationError, "trusted policy has invalid expiry"
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
end
|
|
289
|
-
end
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ace/assign/authority/server"
|
|
4
|
-
require "ace/assign/authority/router"
|
|
5
|
-
require "ace/assign/authority/launch_lifecycle"
|
|
6
|
-
require "ace/assign/authority/endcap"
|
|
7
|
-
require_relative "../molecules/protected_service_policy"
|
|
8
|
-
require "ace/hitl/providers/lab"
|
|
9
|
-
require "ace/hitl/providers/lab/protected_assignment_binding"
|
|
10
|
-
require "ace/hitl/lifecycle/service_publication_binding"
|
|
11
|
-
require "ace/hitl/lifecycle/service"
|
|
12
|
-
|
|
13
|
-
module Ace
|
|
14
|
-
module Lab
|
|
15
|
-
module Organisms
|
|
16
|
-
# The only full-service composition. The deployed map chooses identity
|
|
17
|
-
# and placement; source selects all handlers, policy and evidence readers.
|
|
18
|
-
class AuthorityComposition
|
|
19
|
-
REQUIRED_ENDCAP = %w[submit_candidate export_candidate assign_review accept_review request_service
|
|
20
|
-
begin_dispatch publication_challenge publication_continue claim_service_settlement complete_service complete_no_effect submit_result finish recover bind_inbox
|
|
21
|
-
service_status evidence_fetch].freeze
|
|
22
|
-
|
|
23
|
-
def initialize(authority_id:, deployment: Ace::Assign::Authority::Deployment.load,
|
|
24
|
-
kernel: Ace::Runtime::Molecules::ProtectedLinux.new)
|
|
25
|
-
@authority_id, @deployment, @kernel = authority_id, deployment, kernel
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def build
|
|
29
|
-
@deployment.verify_composition!(@authority_id, composition: "services")
|
|
30
|
-
unless (REQUIRED_ENDCAP - Ace::Assign::Authority::Endcap::OPERATIONS).empty?
|
|
31
|
-
raise InvalidConfigurationError, "full-service source composition is incomplete"
|
|
32
|
-
end
|
|
33
|
-
projects = @deployment.data.fetch("launch_mappings").values
|
|
34
|
-
.select { |map| map.fetch("authority_id") == @authority_id }.map { |map| map.fetch("project_id") }.uniq
|
|
35
|
-
raise InvalidConfigurationError, "full-service authority has no projects" if projects.empty?
|
|
36
|
-
projects.each do |id|
|
|
37
|
-
project = @deployment.project(id)
|
|
38
|
-
unless project["service_receivers"].is_a?(Hash) && !project["service_receivers"].empty?
|
|
39
|
-
raise InvalidConfigurationError, "full-service project has no fixed receiver"
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
journals = {}
|
|
44
|
-
endcap = nil
|
|
45
|
-
projects.each do |id|
|
|
46
|
-
project = @deployment.project(id)
|
|
47
|
-
reader = nil
|
|
48
|
-
journal = Ace::Assign::Molecules::EvidenceJournal.new(repo_root: project.fetch("journal_repository"),
|
|
49
|
-
checkout_root: project.fetch("evidence_checkout_root"), ref: project.fetch("evidence_git_ref"), mode: :protected,
|
|
50
|
-
evidence_reader: ->(reference, record, state, pending) { reader.call(reference, record, state, pending) },
|
|
51
|
-
service_authorizer: ->(existing, replacement, pending) {
|
|
52
|
-
endcap.authorize_service_update!(journal: journals.fetch(id), existing: existing,
|
|
53
|
-
replacement: replacement, pending: pending)
|
|
54
|
-
})
|
|
55
|
-
reader = Ace::Assign::Authority::ServiceEvidence.new(journal: journal)
|
|
56
|
-
journals[id] = journal
|
|
57
|
-
end
|
|
58
|
-
policy = Molecules::ProtectedServicePolicy.new(proposal_resolver: ->(project, reference, binding) {
|
|
59
|
-
journals.fetch(project).proposal_authorize!(reference, binding)
|
|
60
|
-
})
|
|
61
|
-
history = Ace::Assign::Authority::DeploymentHistory.load
|
|
62
|
-
launch = Ace::Assign::Authority::LaunchLifecycle.new(deployment: @deployment, deployment_history: history,
|
|
63
|
-
kernel: @kernel, journals: journals)
|
|
64
|
-
endcap = Ace::Assign::Authority::Endcap.new(deployment: @deployment, launch: launch, kernel: @kernel,
|
|
65
|
-
service_policy: policy, deployment_history: history)
|
|
66
|
-
router = Ace::Assign::Authority::Router.new(launch: launch, handlers: [endcap])
|
|
67
|
-
Ace::Assign::Authority::Server.new(authority_id: @authority_id, deployment: @deployment, kernel: @kernel,
|
|
68
|
-
lifecycle: router, composition: "services", services: [publication_hitl(endcap, journals)])
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
def publication_hitl(endcap, journals)
|
|
74
|
-
provider = Ace::Hitl::Providers::Lab
|
|
75
|
-
policy = provider.grants_policy(grants_path: provider::DEFAULT_GRANTS_PATH)
|
|
76
|
-
unless policy.service_uid == @deployment.authority(@authority_id).fetch("uid")
|
|
77
|
-
raise InvalidConfigurationError, "protected HITL service principal differs from installed authority"
|
|
78
|
-
end
|
|
79
|
-
ordinary = provider::ProtectedAssignmentBinding.new(authority: endcap, kernel: @kernel, journals: journals)
|
|
80
|
-
binding = Ace::Hitl::Lifecycle::ServicePublicationBinding.new(ordinary: ordinary, authority: endcap, kernel: @kernel)
|
|
81
|
-
Ace::Hitl::Lifecycle::Service.new(root: provider::DEFAULT_STORE_ROOT,
|
|
82
|
-
socket_path: provider::DEFAULT_SOCKET_PATH, binding: binding, policy: policy)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "securerandom"
|
|
4
|
-
require "ace/assign/authority/ordinary_client"
|
|
5
|
-
require "ace/assign/authority/ordinary_lifecycle"
|
|
6
|
-
require "ace/assign/organisms/prepared_work_builder"
|
|
7
|
-
require_relative "../molecules/ordinary_owner_config"
|
|
8
|
-
|
|
9
|
-
module Ace
|
|
10
|
-
module Lab
|
|
11
|
-
module Organisms
|
|
12
|
-
# File inputs retain the exact mutation and bytes across response loss.
|
|
13
|
-
# Only Assign's installed owner registers, advances and accepts execution.
|
|
14
|
-
class OrdinaryAssignmentClient
|
|
15
|
-
A = Ace::Assign::Authority
|
|
16
|
-
REQUEST_KEYS = %w[version project_id operation mutation_id params parts].freeze
|
|
17
|
-
|
|
18
|
-
def initialize(project:, config: nil, client_factory: nil)
|
|
19
|
-
@project = project
|
|
20
|
-
@config = config || Molecules::OrdinaryOwnerConfig.load(project: project)
|
|
21
|
-
unless @config.is_a?(Molecules::OrdinaryOwnerConfig) && @config.project == project
|
|
22
|
-
raise ArgumentError, "ordinary assignment project selection differs"
|
|
23
|
-
end
|
|
24
|
-
@client_factory = client_factory || ->(**options) { A::OrdinaryClient.new(**options) }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Preparation produces input only, so it is also available on the host.
|
|
28
|
-
# Reusing an existing output directory never rebuilds or resubmits work.
|
|
29
|
-
def self.prepare(project:, task:, pane:, output:, dependency_reports: [])
|
|
30
|
-
unless project.is_a?(String) && Molecules::OrdinaryOwnerConfig::ID.match?(project) &&
|
|
31
|
-
pane.is_a?(String) && Molecules::OrdinaryOwnerConfig::PANE.match?(pane)
|
|
32
|
-
raise ArgumentError, "preparation requires project and native pane selectors"
|
|
33
|
-
end
|
|
34
|
-
directory = create_output(output)
|
|
35
|
-
prepared = Ace::Assign::Organisms::PreparedWorkBuilder.new(export_root: directory)
|
|
36
|
-
.call(task_ref: task, project_id: project, dependency_reports: dependency_reports)
|
|
37
|
-
definition = JSON.parse(prepared.fetch("definition_bytes"))
|
|
38
|
-
reference = definition.fetch("prepared_work")
|
|
39
|
-
request = {"version" => 1, "project_id" => project, "operation" => "register_assignment",
|
|
40
|
-
"mutation_id" => SecureRandom.hex(16), "params" => {
|
|
41
|
-
"assignment_id" => prepared.fetch("assignment_id"), "pane" => pane, "expected_generation" => 0,
|
|
42
|
-
"definition_digest" => Digest::SHA256.hexdigest(prepared.fetch("definition_bytes")),
|
|
43
|
-
"prepared_head" => prepared.fetch("head"), "prepared_tree" => prepared.fetch("tree"),
|
|
44
|
-
"manifest_sha256" => reference.fetch("manifest_sha256")},
|
|
45
|
-
"parts" => [{"file" => "prepared.bundle", "bytes" => prepared.fetch("bytes"), "sha256" => prepared.fetch("sha256")}]}
|
|
46
|
-
write_file(directory, "definition.json", prepared.fetch("definition_bytes"))
|
|
47
|
-
write_file(directory, "prepared.bundle", prepared.fetch("bundle"))
|
|
48
|
-
write_file(directory, "request.json", JSON.generate(request) + "\n")
|
|
49
|
-
{"project_id" => project, "assignment_id" => prepared.fetch("assignment_id"),
|
|
50
|
-
"scope" => prepared.fetch("scope"), "request" => File.join(directory, "request.json"), "prepared" => true}
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def request(input:, output: nil)
|
|
54
|
-
request = JSON.parse(self.class.read_file(input, 16_384), allow_duplicate_key: false, create_additions: false, max_nesting: 24)
|
|
55
|
-
unless request.is_a?(Hash) && request.keys.sort == REQUEST_KEYS.sort && request["version"].is_a?(Integer) && request["version"] == 1 &&
|
|
56
|
-
request["project_id"] == @project && A::OrdinaryLifecycle::OPERATIONS.include?(request["operation"]) &&
|
|
57
|
-
request["params"].is_a?(Hash) && !request["params"].key?("transfer") && request["parts"].is_a?(Array)
|
|
58
|
-
raise ArgumentError, "retained ordinary request selection is invalid"
|
|
59
|
-
end
|
|
60
|
-
operation = request.fetch("operation")
|
|
61
|
-
read = %w[registration_status assignment_snapshot result_status result_snapshot].include?(operation)
|
|
62
|
-
mutation = request.fetch("mutation_id")
|
|
63
|
-
unless read ? mutation.nil? : mutation.is_a?(String) && Ace::Assign::Molecules::JournalMutation::ID.match?(mutation)
|
|
64
|
-
raise ArgumentError, "retained mutation ID must be explicit; read operations require null"
|
|
65
|
-
end
|
|
66
|
-
transfer = A::OrdinaryLifecycle::TRANSFER_OPERATIONS[operation]
|
|
67
|
-
download = transfer && transfer.fetch(:direction) == :download
|
|
68
|
-
upload = transfer && transfer.fetch(:direction) == :upload
|
|
69
|
-
unless (!!output == !!download) && (request["parts"].empty? || upload)
|
|
70
|
-
raise ArgumentError, "download requires a new --output directory; only uploads accept parts"
|
|
71
|
-
end
|
|
72
|
-
parts = upload && read_parts(request.fetch("parts"), input, transfer.fetch(:purpose))
|
|
73
|
-
# No local metadata is interpreted as successful execution. Download
|
|
74
|
-
# files are merely a projection of the authenticated canonical reply.
|
|
75
|
-
directory = download && self.class.create_output(output)
|
|
76
|
-
client = @client_factory.call(project_id: @project, service: @config.service,
|
|
77
|
-
verify_selection: @config.method(:verify_owner!), scratch_root: directory || nil)
|
|
78
|
-
reply = client.call(operation, request.fetch("params"), mutation_id: request.fetch("mutation_id"),
|
|
79
|
-
upload_parts: parts || nil, download: !!download, purpose: transfer && transfer.fetch(:purpose))
|
|
80
|
-
envelope = {"status" => "ok", "data" => reply.data, "transport" => {"replayed" => reply.replayed}}
|
|
81
|
-
if download
|
|
82
|
-
files = reply.parts.each_with_index.map do |bytes, index|
|
|
83
|
-
name = format("part-%02d.bytes", index)
|
|
84
|
-
self.class.write_file(directory, name, bytes)
|
|
85
|
-
{"file" => File.join(directory, name), "bytes" => bytes.bytesize, "sha256" => Digest::SHA256.hexdigest(bytes)}
|
|
86
|
-
end
|
|
87
|
-
envelope["files"] = files
|
|
88
|
-
self.class.write_file(directory, "response.json", JSON.generate(envelope) + "\n")
|
|
89
|
-
end
|
|
90
|
-
envelope
|
|
91
|
-
rescue JSON::ParserError
|
|
92
|
-
raise ArgumentError, "retained request is not valid bounded JSON"
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def self.read_file(path, limit)
|
|
96
|
-
File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |file|
|
|
97
|
-
before = file.stat
|
|
98
|
-
raise ArgumentError, "retained input is not a bounded regular file" unless before.file? && before.size.between?(0, limit)
|
|
99
|
-
bytes = (file.read(limit + 1) || "").b
|
|
100
|
-
after, selected = file.stat, File.lstat(path)
|
|
101
|
-
identity = ->(stat) { [stat.dev, stat.ino, stat.size, stat.mtime, stat.ctime] }
|
|
102
|
-
unless bytes.bytesize == before.size && identity.call(before) == identity.call(after) &&
|
|
103
|
-
identity.call(after) == identity.call(selected) && !selected.symlink?
|
|
104
|
-
raise ArgumentError, "retained input changed during read"
|
|
105
|
-
end
|
|
106
|
-
bytes.freeze
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def self.create_output(path)
|
|
111
|
-
raise ArgumentError, "output must be a new directory path" unless path.is_a?(String) && !path.empty?
|
|
112
|
-
directory = File.expand_path(path)
|
|
113
|
-
Dir.mkdir(directory, 0o700)
|
|
114
|
-
A::PrivateDirectory.verify!(directory)
|
|
115
|
-
directory
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def self.write_file(directory, name, bytes)
|
|
119
|
-
File.open(File.join(directory, name), File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0o600) do |file|
|
|
120
|
-
file.write(bytes)
|
|
121
|
-
file.flush
|
|
122
|
-
file.fsync
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
private
|
|
127
|
-
|
|
128
|
-
def read_parts(entries, input, purpose)
|
|
129
|
-
total, count, per_part = A::TransferCodec::LIMITS.fetch(purpose)
|
|
130
|
-
unless entries.size.between?(1, count) && entries.all? { |entry|
|
|
131
|
-
entry.is_a?(Hash) && entry.keys.sort == %w[bytes file sha256] && entry["file"].is_a?(String) &&
|
|
132
|
-
entry["file"].bytesize.between?(1, 4096) && !entry["file"].include?("\0") &&
|
|
133
|
-
entry["bytes"].is_a?(Integer) && entry["bytes"].between?(0, per_part) &&
|
|
134
|
-
entry["sha256"].is_a?(String) && A::TransferCodec::SHA256.match?(entry["sha256"])
|
|
135
|
-
} && entries.sum { |entry| entry.fetch("bytes") } <= total
|
|
136
|
-
raise ArgumentError, "retained upload inventory is invalid"
|
|
137
|
-
end
|
|
138
|
-
entries.map do |entry|
|
|
139
|
-
bytes = self.class.read_file(File.expand_path(entry.fetch("file"), File.dirname(File.expand_path(input))), per_part)
|
|
140
|
-
unless bytes.bytesize == entry.fetch("bytes") && Digest::SHA256.hexdigest(bytes) == entry.fetch("sha256")
|
|
141
|
-
raise ArgumentError, "retained upload bytes changed; preserve the original mutation"
|
|
142
|
-
end
|
|
143
|
-
bytes
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
end
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ace/assign/authority/ordinary_lifecycle"
|
|
4
|
-
require "ace/assign/authority/ordinary_server"
|
|
5
|
-
require "ace/assign/authority/router"
|
|
6
|
-
require "ace/hitl/providers/lab"
|
|
7
|
-
require "ace/hitl/lifecycle/service"
|
|
8
|
-
require "ace/hitl/lifecycle/tls_authority"
|
|
9
|
-
require "ace/hitl/lifecycle/tls_service"
|
|
10
|
-
require_relative "../molecules/ordinary_owner_admission"
|
|
11
|
-
require_relative "../molecules/ordinary_listener"
|
|
12
|
-
require_relative "ordinary_merge_receiver"
|
|
13
|
-
|
|
14
|
-
module Ace
|
|
15
|
-
module Lab
|
|
16
|
-
module Organisms
|
|
17
|
-
# Ordinary delivery has one installed journal writer and exclusion owner.
|
|
18
|
-
# Existing Assign, HITL and merge protocols share that process lifetime;
|
|
19
|
-
# no requester cache or daemon working directory chooses accepted state.
|
|
20
|
-
class OrdinaryAuthorityComposition
|
|
21
|
-
def initialize(receiver_config:, owner_config: nil)
|
|
22
|
-
unless receiver_config.is_a?(Molecules::OrdinaryReceiverConfig)
|
|
23
|
-
raise ArgumentError, "ordinary composition needs installed receiver selection"
|
|
24
|
-
end
|
|
25
|
-
@receiver = receiver_config
|
|
26
|
-
@config = owner_config || Molecules::OrdinaryOwnerConfig.load(project: @receiver.project)
|
|
27
|
-
unless @config.is_a?(Molecules::OrdinaryOwnerConfig)
|
|
28
|
-
raise ArgumentError, "ordinary composition needs installed assignment selection"
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def build
|
|
33
|
-
@config.verify_listener!
|
|
34
|
-
policy_document = Molecules::GrantResolver.trusted_document(@receiver.policy_path)
|
|
35
|
-
verify_join!(policy_document)
|
|
36
|
-
admission = Molecules::OrdinaryOwnerAdmission.new(config: @config)
|
|
37
|
-
reader = nil
|
|
38
|
-
journal = Ace::Assign::Molecules::EvidenceJournal.new(repo_root: @config.journal.fetch("repository"),
|
|
39
|
-
ref: @config.journal.fetch("ref"), checkout_root: @config.journal.fetch("checkout_root"),
|
|
40
|
-
evidence_reader: ->(reference, record, state, pending) { reader.call(reference, record, state, pending) })
|
|
41
|
-
reader = Ace::Assign::Authority::ServiceEvidence.new(journal: journal)
|
|
42
|
-
exclusion = Ace::Assign::Molecules::LifecycleExclusion.new(root: @config.lifecycle_root)
|
|
43
|
-
coordinator = Ace::Assign::Organisms::AttemptCoordinator.new(cache_base: @config.assignment_root,
|
|
44
|
-
repo_root: @config.journal.fetch("repository"), journal: journal, lifecycle_exclusion: exclusion,
|
|
45
|
-
recorded_binding_observer: admission.method(:recorded_binding!))
|
|
46
|
-
lifecycle = Ace::Assign::Authority::OrdinaryLifecycle.new(project: @config.project,
|
|
47
|
-
admission: admission, journal: journal, exclusion: exclusion, input_root: @config.assignment_root)
|
|
48
|
-
router = Ace::Assign::Authority::Router.new(handlers: [lifecycle])
|
|
49
|
-
merge = OrdinaryMergeReceiver.new(config: @receiver, coordinator: coordinator,
|
|
50
|
-
policy_document: policy_document, owner_guard: -> { @config.selected_principal!(@receiver.principal_uid) },
|
|
51
|
-
merge_factory: ->(document) {
|
|
52
|
-
Molecules::OrdinaryMergeRead.new(repo_path: @receiver.repo_path, base_ref: document.fetch("base_ref"),
|
|
53
|
-
root: @receiver.repo_path, common_dir: @config.worktrees.fetch("common_dir"))
|
|
54
|
-
})
|
|
55
|
-
listener = Molecules::OrdinaryListener.new(socket_path: @receiver.socket_path,
|
|
56
|
-
peer_uid: @receiver.principal_uid, socket_gid: @receiver.socket_gid, handler: merge,
|
|
57
|
-
logger: ->(message) { warn message })
|
|
58
|
-
Ace::Assign::Authority::OrdinaryServer.new(admission: admission, lifecycle: router,
|
|
59
|
-
services: hitl_services(coordinator) + [listener])
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
def verify_join!(document)
|
|
65
|
-
configured = Molecules::ServicePolicy.new(document).ordinary_receiver!(@config.project)
|
|
66
|
-
expected = {"receiver_id" => @receiver.receiver_id, "socket_path" => @receiver.socket_path,
|
|
67
|
-
"receiver_uid" => @config.service.fetch("uid"), "principal" => @receiver.principal,
|
|
68
|
-
"journal_repository" => @config.journal.fetch("repository"), "journal_ref" => @config.journal.fetch("ref")}
|
|
69
|
-
unless @receiver.project == @config.project && @receiver.receiver_uid == @config.service.fetch("uid") &&
|
|
70
|
-
configured && configured.slice(*expected.keys) == expected &&
|
|
71
|
-
@config.participants.key?(@receiver.principal_uid.to_s) &&
|
|
72
|
-
![@config.service.fetch("socket_path"), @config.native.fetch("socket_path"), @config.hitl.fetch("socket_path")].include?(@receiver.socket_path)
|
|
73
|
-
raise InvalidConfigurationError, "ordinary receiver and assignment owner selections differ"
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def hitl_services(coordinator)
|
|
78
|
-
selected = @config.hitl
|
|
79
|
-
policy = Ace::Hitl::Providers::Lab.grants_policy(grants_path: selected.fetch("grants_path"))
|
|
80
|
-
unless policy.service_uid == @config.service.fetch("uid")
|
|
81
|
-
raise InvalidConfigurationError, "ordinary HITL service principal differs from assignment owner"
|
|
82
|
-
end
|
|
83
|
-
guard = lambda do
|
|
84
|
-
@config.verify_owner!
|
|
85
|
-
unless policy.service_uid == @config.service.fetch("uid")
|
|
86
|
-
raise Ace::Hitl::Lifecycle::BindingError, "installed HITL service principal changed"
|
|
87
|
-
end
|
|
88
|
-
rescue Ace::Lab::InvalidConfigurationError, Ace::Assign::Error => error
|
|
89
|
-
raise Ace::Hitl::Lifecycle::BindingError, error.message
|
|
90
|
-
end
|
|
91
|
-
binding = Ace::Hitl::Providers::Lab::AssignmentBinding.new(coordinator: coordinator,
|
|
92
|
-
project: @config.project, owner_guard: guard)
|
|
93
|
-
# Unix requesters and the existing Hermes mTLS transport share the
|
|
94
|
-
# same owner and one in-memory OTP vault. No second daemon opens the
|
|
95
|
-
# journal and no synthetic Unix principal represents a certificate.
|
|
96
|
-
vault = Ace::Hitl::Lifecycle::OtpVault::MemoryVault.new
|
|
97
|
-
services = [Ace::Hitl::Lifecycle::Service.new(root: selected.fetch("root"),
|
|
98
|
-
socket_path: selected.fetch("socket_path"), group: selected.fetch("group"),
|
|
99
|
-
binding: binding, policy: policy, vault: vault)]
|
|
100
|
-
if selected.key?("tls_authority")
|
|
101
|
-
authority = Ace::Hitl::Lifecycle::TlsAuthority.load(selected.fetch("tls_authority"))
|
|
102
|
-
verify_tls_identity!(authority)
|
|
103
|
-
services << Ace::Hitl::Lifecycle::TlsService.new(root: selected.fetch("root"),
|
|
104
|
-
binding: binding, project: @config.project, credential: authority.credential,
|
|
105
|
-
ssl_context: authority.ssl_context!, listen: authority.listen, port: authority.port,
|
|
106
|
-
group: selected.fetch("group"), vault: vault, decision_policy: policy)
|
|
107
|
-
end
|
|
108
|
-
services
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def verify_tls_identity!(authority)
|
|
112
|
-
unless authority.project_id == @config.project &&
|
|
113
|
-
Etc.getpwnam(authority.service_user).uid == @config.service.fetch("uid") &&
|
|
114
|
-
Etc.getgrnam(authority.service_group).gid == @config.service.fetch("gid")
|
|
115
|
-
raise InvalidConfigurationError, "ordinary HITL TLS project or service identity differs"
|
|
116
|
-
end
|
|
117
|
-
rescue ArgumentError
|
|
118
|
-
raise InvalidConfigurationError, "ordinary HITL TLS service account is unavailable"
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|