ace-lab 0.9.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 -104
- data/README.md +66 -100
- data/docs/usage.md +529 -592
- 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 -23
- 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 +34 -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 -108
- 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 -92
- data/lib/ace/lab/cli/commands/command_execute.rb +0 -28
- data/lib/ace/lab/cli/commands/run.rb +0 -65
- 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/command_owner_admission.rb +0 -183
- data/lib/ace/lab/molecules/command_policy.rb +0 -158
- 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 -152
- 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/command_executor.rb +0 -220
- data/lib/ace/lab/organisms/command_run_client.rb +0 -131
- data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -120
- data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -153
- data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -679
- 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,126 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "socket"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Lab
|
|
8
|
-
module Molecules
|
|
9
|
-
# Admission transport between the ordinary fixed merge producer and the
|
|
10
|
-
# installed ordinary receiver: one bounded JSON document each way over
|
|
11
|
-
# a configured unix stream socket with a fixed deadline. Authentication
|
|
12
|
-
# is mutual through OS peer credentials — the producer verifies the receiver's
|
|
13
|
-
# configured UID and transmits the verified local caller identity, so
|
|
14
|
-
# no caller-selected credential, token or endpoint exists. This is not
|
|
15
|
-
# an HTTP transport; a lost or unauthenticated response is a
|
|
16
|
-
# caller-side blocker (ordinary_merge_unavailable), never a refusal.
|
|
17
|
-
class OrdinarySocket
|
|
18
|
-
LIMIT = 16_384
|
|
19
|
-
HANDSHAKE = "ace-lab/ordinary-merge/v1"
|
|
20
|
-
|
|
21
|
-
# @param socket_path [String] installed receiver endpoint (configuration)
|
|
22
|
-
# @param receiver_uid [Integer] the only UID the receiver may run as
|
|
23
|
-
# @param deadline [Time] absolute deadline for connect/write/read
|
|
24
|
-
def initialize(socket_path:, receiver_uid:, deadline: Time.now.utc + 30)
|
|
25
|
-
@socket_path = socket_path
|
|
26
|
-
@receiver_uid = receiver_uid
|
|
27
|
-
@deadline = deadline
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Submit one bounded request document; returns the parsed response
|
|
31
|
-
# document. Any framing, deadline or identity failure raises
|
|
32
|
-
# OrdinaryUnavailable so the producer can classify the blocker.
|
|
33
|
-
def submit(document)
|
|
34
|
-
payload = JSON.generate(document)
|
|
35
|
-
raise OrdinaryUnavailable, "request document exceeds the wire bound" if payload.bytesize > LIMIT
|
|
36
|
-
socket = nil
|
|
37
|
-
begin
|
|
38
|
-
socket = UNIXSocket.new(@socket_path)
|
|
39
|
-
verify_peer!(socket)
|
|
40
|
-
write_all!(socket, "#{HANDSHAKE} #{payload.bytesize}\n#{payload}")
|
|
41
|
-
response = read_response!(socket)
|
|
42
|
-
rescue OrdinaryUnavailable
|
|
43
|
-
raise
|
|
44
|
-
rescue SystemCallError, IOError, SocketError, NotImplementedError => e
|
|
45
|
-
raise OrdinaryUnavailable, "receiver transport failed: #{e.class}"
|
|
46
|
-
ensure
|
|
47
|
-
begin
|
|
48
|
-
socket&.close
|
|
49
|
-
rescue IOError
|
|
50
|
-
nil
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
response
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
class OrdinaryUnavailable < ::StandardError; end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
def verify_peer!(socket)
|
|
61
|
-
peer_uid, = socket.getpeereid
|
|
62
|
-
return if peer_uid == @receiver_uid
|
|
63
|
-
raise OrdinaryUnavailable, "receiver endpoint identity is not the installed receiver"
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def write_all!(socket, bytes)
|
|
67
|
-
written = 0
|
|
68
|
-
while written < bytes.bytesize
|
|
69
|
-
begin
|
|
70
|
-
raise OrdinaryUnavailable, "receiver write deadline expired" if expired?
|
|
71
|
-
written += socket.write_nonblock(bytes.byteslice(written..))
|
|
72
|
-
rescue IO::WaitWritable
|
|
73
|
-
socket.wait_writable(remaining!) or raise OrdinaryUnavailable, "receiver write deadline expired"
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def read_response!(socket)
|
|
79
|
-
header = read_line!(socket)
|
|
80
|
-
match = /\A#{Regexp.escape(HANDSHAKE)} (\d{1,5})\n\z/o.match(header)
|
|
81
|
-
raise OrdinaryUnavailable, "receiver response header is invalid" unless match
|
|
82
|
-
length = Integer(match[1])
|
|
83
|
-
raise OrdinaryUnavailable, "receiver response exceeds the wire bound" if length > LIMIT
|
|
84
|
-
body = read_exact!(socket, length)
|
|
85
|
-
JSON.parse(body, allow_duplicate_key: false, create_additions: false, max_nesting: 16)
|
|
86
|
-
rescue JSON::ParserError
|
|
87
|
-
raise OrdinaryUnavailable, "receiver response is not valid JSON"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def read_line!(socket)
|
|
91
|
-
line = +""
|
|
92
|
-
until line.end_with?("\n")
|
|
93
|
-
raise OrdinaryUnavailable, "receiver response header is unbounded" if line.bytesize > 128
|
|
94
|
-
line << read_exact!(socket, 1)
|
|
95
|
-
end
|
|
96
|
-
line
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def read_exact!(socket, length)
|
|
100
|
-
buffer = +""
|
|
101
|
-
while buffer.bytesize < length
|
|
102
|
-
begin
|
|
103
|
-
raise OrdinaryUnavailable, "receiver read deadline expired" if expired?
|
|
104
|
-
chunk = socket.read_nonblock(length - buffer.bytesize)
|
|
105
|
-
raise OrdinaryUnavailable, "receiver closed the connection" if chunk.nil?
|
|
106
|
-
buffer << chunk
|
|
107
|
-
rescue IO::WaitReadable
|
|
108
|
-
socket.wait_readable(remaining!) or raise OrdinaryUnavailable, "receiver read deadline expired"
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
buffer
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def expired?
|
|
115
|
-
Time.now.utc >= @deadline
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def remaining!
|
|
119
|
-
left = @deadline - Time.now.utc
|
|
120
|
-
raise OrdinaryUnavailable, "receiver deadline expired" if left <= 0
|
|
121
|
-
left
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ace/assign/molecules/installed_journal_read"
|
|
4
|
-
|
|
5
|
-
module Ace
|
|
6
|
-
module Lab
|
|
7
|
-
module Molecules
|
|
8
|
-
# Observe a mutable ordinary worker checkout. The root-installed project
|
|
9
|
-
# selection supplies both boundaries; caller input never selects a Git
|
|
10
|
-
# repository. This is historical observation, not a checkout freeze.
|
|
11
|
-
class OrdinaryWorktreeRead
|
|
12
|
-
def initialize(root:, common_dir:, deadline:)
|
|
13
|
-
@root, @common, @deadline = root, common_dir, deadline
|
|
14
|
-
[root, common_dir].each { |path| absolute!(path) }
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def observe(cwd)
|
|
18
|
-
absolute!(cwd)
|
|
19
|
-
remaining
|
|
20
|
-
unless within?(cwd, @root)
|
|
21
|
-
unavailable!("worker working directory is outside installed worktrees")
|
|
22
|
-
end
|
|
23
|
-
before = selection(cwd)
|
|
24
|
-
head = git(before, "rev-parse", "--verify", "HEAD^{commit}").strip
|
|
25
|
-
unavailable!("worker HEAD is unavailable") unless head.match?(/\A[0-9a-f]{40}\z/)
|
|
26
|
-
after = selection(cwd)
|
|
27
|
-
unless before == after && git(after, "rev-parse", "--verify", "HEAD^{commit}").strip == head
|
|
28
|
-
unavailable!("worker checkout changed during observation")
|
|
29
|
-
end
|
|
30
|
-
{"cwd" => cwd, "worktree" => before.fetch("worktree"), "git_dir" => before.fetch("git_dir"),
|
|
31
|
-
"common_dir" => @common, "head" => head}
|
|
32
|
-
rescue SystemCallError, IOError, Timeout::Error, Ace::Herdr::Molecules::BoundedProcess::PostLaunchError
|
|
33
|
-
unavailable!("worker checkout cannot be observed")
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def ref_head(cwd, ref)
|
|
37
|
-
unless ref.is_a?(String) && ref.match?(%r{\Arefs/[a-zA-Z0-9_./-]+\z}) && !ref.include?("..")
|
|
38
|
-
unavailable!("selected Git ref is invalid")
|
|
39
|
-
end
|
|
40
|
-
with_selected_read(cwd) do |selected|
|
|
41
|
-
head = git(selected, "rev-parse", "--verify", "#{ref}^{commit}").strip
|
|
42
|
-
unavailable!("selected ref is not a commit") unless head.match?(/\A[0-9a-f]{40}\z/)
|
|
43
|
-
head
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def commit?(cwd, head)
|
|
48
|
-
exact_head!(head)
|
|
49
|
-
with_selected_read(cwd) do |selected|
|
|
50
|
-
git(selected, "rev-parse", "--verify", "--quiet", "#{head}^{commit}", allowed_status: [0, 1], result: true).status.success?
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def ancestor?(cwd, ancestor, descendant)
|
|
55
|
-
exact_head!(ancestor); exact_head!(descendant)
|
|
56
|
-
with_selected_read(cwd) do |selected|
|
|
57
|
-
git(selected, "merge-base", "--is-ancestor", ancestor, descendant, allowed_status: [0, 1], result: true).status.success?
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def exact_head!(head)
|
|
64
|
-
unavailable!("selected Git head is invalid") unless head.is_a?(String) && head.match?(/\A[0-9a-f]{40}\z/)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def with_selected_read(cwd)
|
|
68
|
-
absolute!(cwd)
|
|
69
|
-
unavailable!("selected repository leaves installed worktrees") unless within?(cwd, @root)
|
|
70
|
-
before = selection(cwd)
|
|
71
|
-
value = yield before
|
|
72
|
-
unavailable!("selected repository changed during read") unless before == selection(cwd)
|
|
73
|
-
value
|
|
74
|
-
rescue SystemCallError, IOError, Timeout::Error, Ace::Herdr::Molecules::BoundedProcess::PostLaunchError => error
|
|
75
|
-
unavailable!("selected repository cannot be observed (#{error.class})")
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def selection(cwd)
|
|
79
|
-
directory!(cwd)
|
|
80
|
-
directory!(@common)
|
|
81
|
-
worktree = cwd
|
|
82
|
-
loop do
|
|
83
|
-
break if File.exist?(File.join(worktree, ".git")) || File.symlink?(File.join(worktree, ".git"))
|
|
84
|
-
unavailable!("worker checkout is unavailable") if worktree == @root
|
|
85
|
-
worktree = File.dirname(worktree)
|
|
86
|
-
end
|
|
87
|
-
marker = File.join(worktree, ".git")
|
|
88
|
-
metadata = {}
|
|
89
|
-
git_dir = if File.lstat(marker).directory?
|
|
90
|
-
directory!(marker)
|
|
91
|
-
unavailable!("worker belongs to another project") unless marker == @common
|
|
92
|
-
marker
|
|
93
|
-
else
|
|
94
|
-
bytes = regular!(marker, metadata, 4096)
|
|
95
|
-
match = bytes.match(/\Agitdir: ([^\n\0]+)\n?\z/)
|
|
96
|
-
unavailable!("worker Git pointer is invalid") unless match
|
|
97
|
-
selected = File.expand_path(match[1], worktree)
|
|
98
|
-
unless File.dirname(selected) == File.join(@common, "worktrees")
|
|
99
|
-
unavailable!("worker Git pointer leaves installed project")
|
|
100
|
-
end
|
|
101
|
-
directory!(selected)
|
|
102
|
-
common = regular!(File.join(selected, "commondir"), metadata, 4096).strip
|
|
103
|
-
unavailable!("worker common directory differs") unless File.expand_path(common, selected) == @common
|
|
104
|
-
back = regular!(File.join(selected, "gitdir"), metadata, 4096).strip
|
|
105
|
-
unavailable!("worker Git backpointer differs") unless back == marker
|
|
106
|
-
selected
|
|
107
|
-
end
|
|
108
|
-
@entries = 0
|
|
109
|
-
%w[objects refs].each { |name| storage!(File.join(@common, name)) }
|
|
110
|
-
%w[commondir shallow reftable info/grafts objects/info/alternates objects/info/http-alternates].each do |name|
|
|
111
|
-
path = File.join(@common, name)
|
|
112
|
-
unavailable!("worker object storage is redirected") if File.exist?(path) || File.symlink?(path)
|
|
113
|
-
end
|
|
114
|
-
regular!(File.join(git_dir, "HEAD"), metadata, 4096)
|
|
115
|
-
packed = File.join(@common, "packed-refs")
|
|
116
|
-
storage!(packed) if File.exist?(packed) || File.symlink?(packed)
|
|
117
|
-
local_refs = File.join(git_dir, "refs")
|
|
118
|
-
storage!(local_refs) if git_dir != @common && (File.exist?(local_refs) || File.symlink?(local_refs))
|
|
119
|
-
config!(File.join(@common, "config"), metadata)
|
|
120
|
-
local = File.join(git_dir, "config.worktree")
|
|
121
|
-
config!(local, metadata) if File.exist?(local) || File.symlink?(local)
|
|
122
|
-
{"worktree" => worktree, "git_dir" => git_dir, "metadata" => metadata,
|
|
123
|
-
"directories" => [cwd, worktree, git_dir, @common].to_h { |path| [path, identity(File.lstat(path))] }}
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def config!(path, metadata)
|
|
127
|
-
bytes = regular!(path, metadata, 65_536)
|
|
128
|
-
output = run(["config", "--no-includes", "--null", "--list", "--file", "/dev/stdin"], stdin: bytes)
|
|
129
|
-
output.split("\0").each do |entry|
|
|
130
|
-
key, value = entry.split("\n", 2)
|
|
131
|
-
if key.start_with?("include.", "includeif.", "extensions.") || key == "core.worktree" ||
|
|
132
|
-
key == "core.repositoryformatversion" && value != "0"
|
|
133
|
-
unavailable!("worker Git configuration redirects project storage")
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def git(selected, *argv, allowed_status: [0], result: false)
|
|
139
|
-
captured = run_result(["--git-dir=#{selected.fetch('git_dir')}", "--work-tree=#{selected.fetch('worktree')}",
|
|
140
|
-
"-c", "safe.directory=#{selected.fetch('worktree')}", "-c", "core.hooksPath=/dev/null",
|
|
141
|
-
"-c", "core.fsmonitor=false", "-c", "protocol.allow=never", *argv],
|
|
142
|
-
environment: {"GIT_COMMON_DIR" => @common, "GIT_OBJECT_DIRECTORY" => File.join(@common, "objects")}, allowed_status: allowed_status)
|
|
143
|
-
result ? captured : captured.stdout
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def run(argv, stdin: "", environment: {})
|
|
147
|
-
run_result(argv, stdin: stdin, environment: environment).stdout
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def run_result(argv, stdin: "", environment: {}, allowed_status: [0])
|
|
151
|
-
result = Ace::Herdr::Molecules::BoundedProcess.call(["/usr/bin/git", "--no-pager", *argv],
|
|
152
|
-
environment: Ace::Assign::Molecules::InstalledJournalRead::ENVIRONMENT.merge(environment), chdir: "/",
|
|
153
|
-
stdin_data: stdin, timeout_s: remaining, output_limit: 65_536, cleanup_group: true)
|
|
154
|
-
unavailable!("worker Git read failed or exceeded its bound") unless allowed_status.include?(result.status.exitstatus) && !result.oversized
|
|
155
|
-
result
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def regular!(path, metadata, limit)
|
|
159
|
-
directory!(File.dirname(path))
|
|
160
|
-
File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |file|
|
|
161
|
-
before = file.stat
|
|
162
|
-
unavailable!("worker metadata must be a bounded regular file") unless before.file? && before.size <= limit
|
|
163
|
-
bytes = file.read(limit + 1)
|
|
164
|
-
unless bytes.bytesize <= limit && stamp(before) == stamp(file.stat) && stamp(before) == stamp(File.lstat(path))
|
|
165
|
-
unavailable!("worker metadata changed during read")
|
|
166
|
-
end
|
|
167
|
-
metadata[path] = [stamp(before), bytes]
|
|
168
|
-
bytes
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def directory!(path)
|
|
173
|
-
current = path
|
|
174
|
-
loop do
|
|
175
|
-
remaining
|
|
176
|
-
stat = File.lstat(current)
|
|
177
|
-
unavailable!("worker directory is redirected") unless stat.directory? && !stat.symlink?
|
|
178
|
-
break if current == "/"
|
|
179
|
-
current = File.dirname(current)
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def storage!(path)
|
|
184
|
-
remaining
|
|
185
|
-
@entries += 1
|
|
186
|
-
unavailable!("worker storage inventory exceeds its bound") if @entries > Ace::Assign::Molecules::InstalledJournalRead::ENTRY_LIMIT
|
|
187
|
-
stat = File.lstat(path)
|
|
188
|
-
unavailable!("worker storage is redirected") unless !stat.symlink? && (stat.file? || stat.directory?)
|
|
189
|
-
Dir.each_child(path) { |name| storage!(File.join(path, name)) } if stat.directory?
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def absolute!(path)
|
|
193
|
-
unless path.is_a?(String) && path.bytesize.between?(1, 4096) && !path.include?("\0") &&
|
|
194
|
-
path.start_with?("/") && File.expand_path(path) == path
|
|
195
|
-
unavailable!("worker path is invalid")
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def within?(path, parent) = path == parent || path.start_with?(parent + "/")
|
|
200
|
-
def identity(stat) = [stat.dev, stat.ino]
|
|
201
|
-
def stamp(stat) = [stat.dev, stat.ino, stat.size, stat.mtime, stat.ctime]
|
|
202
|
-
|
|
203
|
-
def remaining
|
|
204
|
-
unless @deadline.is_a?(Numeric) && @deadline.finite? && (left = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)).positive?
|
|
205
|
-
unavailable!("worker observation deadline elapsed")
|
|
206
|
-
end
|
|
207
|
-
left
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def unavailable!(reason)
|
|
211
|
-
raise Ace::Assign::AttemptErrors::EvidenceUnavailable, reason
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
end
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ace/assign/authority/service_evidence"
|
|
4
|
-
require_relative "protected_cleanup_owner_client"
|
|
5
|
-
require "ace/assign/authority/endcap"
|
|
6
|
-
require "ace/assign/authority/launch_lifecycle"
|
|
7
|
-
|
|
8
|
-
module Ace
|
|
9
|
-
module Lab
|
|
10
|
-
module Molecules
|
|
11
|
-
# Only the fixed listener supplies the installed journal snapshot and its
|
|
12
|
-
# independently observed self binding. A frame selects neither owner.
|
|
13
|
-
class ProtectedCleanupOwnerAdmission
|
|
14
|
-
FIELDS = %w[schema kind request_id input_digest claim_binding request_event_digest
|
|
15
|
-
dispatch_event_digest input operation_owner_binding_digest].freeze
|
|
16
|
-
INSPECT_FIELDS = (FIELDS + %w[challenge_ref]).freeze
|
|
17
|
-
|
|
18
|
-
def initialize(deployment:, authority_id:, mapping_id:, service_id:, kernel:, preview_policy: nil)
|
|
19
|
-
@deployment, @authority_id, @kernel = deployment, authority_id, kernel
|
|
20
|
-
@mapping_id, @service_id = mapping_id, service_id
|
|
21
|
-
@preview_policy = preview_policy
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def identity_reader!(peer)
|
|
25
|
-
authority = @deployment.authority(@authority_id)
|
|
26
|
-
if peer.slice("uid", "gid", "groups") == authority.slice("uid", "gid", "groups")
|
|
27
|
-
@kernel.live!(peer)
|
|
28
|
-
return true
|
|
29
|
-
end
|
|
30
|
-
receiver!(peer)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def connection_group!(gid)
|
|
34
|
-
authority = @deployment.authority(@authority_id)
|
|
35
|
-
map = @deployment.mapping(@mapping_id)
|
|
36
|
-
project = @deployment.project(map.fetch("project_id"))
|
|
37
|
-
receiver = project.fetch("service_receivers").fetch(@service_id)
|
|
38
|
-
credentials = project.fetch("peer_credentials").fetch(receiver.fetch("executor_uid").to_s)
|
|
39
|
-
unless gid.is_a?(Integer) && gid.positive? && [authority, credentials].all? { |principal| principal.fetch("gid") == gid || principal.fetch("groups").include?(gid) }
|
|
40
|
-
raise SecurityError, "cleanup connection group differs from installed principals"
|
|
41
|
-
end
|
|
42
|
-
true
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def receiver!(peer)
|
|
46
|
-
map = @deployment.mapping(@mapping_id)
|
|
47
|
-
project = @deployment.project(map.fetch("project_id"))
|
|
48
|
-
receiver = project.fetch("service_receivers").fetch(@service_id)
|
|
49
|
-
credentials = project.fetch("peer_credentials").fetch(receiver.fetch("executor_uid").to_s)
|
|
50
|
-
unless map.fetch("authority_id") == @authority_id && peer.fetch("uid").is_a?(Integer) && peer.fetch("uid").positive? &&
|
|
51
|
-
peer.fetch("uid") == receiver.fetch("executor_uid") && peer.fetch("gid") == credentials.fetch("gid") &&
|
|
52
|
-
peer.fetch("groups") == credentials.fetch("groups")
|
|
53
|
-
raise SecurityError, "cleanup installed receiver differs"
|
|
54
|
-
end
|
|
55
|
-
@kernel.live!(peer)
|
|
56
|
-
true
|
|
57
|
-
rescue KeyError, TypeError
|
|
58
|
-
raise SecurityError, "cleanup installed receiver unavailable"
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def preview!(frame:, peer:, operation_owner_binding:, journal:)
|
|
62
|
-
receiver!(peer)
|
|
63
|
-
Atoms::ProtectedWorkspacePruneInput.object!(frame, %w[schema kind intent maintenance_context])
|
|
64
|
-
raise SecurityError, "cleanup preview kind differs" unless frame.values_at("schema", "kind") == [ProtectedCleanupOwnerClient::SCHEMA, "preview"]
|
|
65
|
-
intent = Atoms::ProtectedWorkspacePrunePreview.intent!(frame.fetch("intent"))
|
|
66
|
-
supplied = Atoms::ProtectedWorkspacePrunePreview.context!(frame.fetch("maintenance_context"))
|
|
67
|
-
maintenance = supplied.fetch("maintenance")
|
|
68
|
-
map = @deployment.mapping(@mapping_id)
|
|
69
|
-
unless intent.fetch("maintenance") == maintenance && maintenance.fetch("mapping_id") == @mapping_id &&
|
|
70
|
-
maintenance.fetch("project_id") == map.fetch("project_id") && map.fetch("authority_id") == @authority_id
|
|
71
|
-
raise SecurityError, "cleanup preview installed scope differs"
|
|
72
|
-
end
|
|
73
|
-
raise SecurityError, "cleanup preview policy composition unavailable" unless @preview_policy
|
|
74
|
-
launch = Ace::Assign::Authority::LaunchLifecycle.new(deployment: @deployment, kernel: @kernel)
|
|
75
|
-
owner = Ace::Assign::Authority::Endcap.new(deployment: @deployment, launch: launch, kernel: @kernel, service_policy: @preview_policy)
|
|
76
|
-
params = maintenance.except("project_id").merge("service_id" => @service_id,
|
|
77
|
-
"worker_process_binding" => supplied.fetch("caller_process_binding"))
|
|
78
|
-
selected = owner.workspace_prune_preview_context_at!(journal: journal, params: params, map: map, peer: peer, role: :executor)
|
|
79
|
-
raise SecurityError, "cleanup preview canonical selection changed" unless selected == supplied
|
|
80
|
-
Atoms::ProtectedWorkspacePruneInput.freeze_value({"intent" => intent, "maintenance_context" => selected,
|
|
81
|
-
"operation_owner_binding" => operation_owner_binding})
|
|
82
|
-
rescue KeyError, TypeError, ArgumentError
|
|
83
|
-
raise SecurityError, "cleanup preview canonical selection unavailable"
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def admit!(frame:, peer:, operation_owner_binding:, journal:)
|
|
87
|
-
admit_original!(frame: frame, peer: peer, operation_owner_binding: operation_owner_binding, journal: journal, purpose: :execute)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def inspect!(frame:, peer:, operation_owner_binding:, journal:)
|
|
91
|
-
context = admit_original!(frame: frame, peer: peer, operation_owner_binding: operation_owner_binding, journal: journal, purpose: :inspect)
|
|
92
|
-
reference = frame.fetch("challenge_ref")
|
|
93
|
-
Atoms::ProtectedWorkspacePruneInput.object!(reference, %w[challenge_event_digest])
|
|
94
|
-
Atoms::ProtectedWorkspacePruneInput.digest!(reference.fetch("challenge_event_digest"))
|
|
95
|
-
challenge = Ace::Assign::Authority::ServiceEvidence.new(journal: journal).challenge!(context.fetch("record"), pending: {commit: context.fetch("commit")})
|
|
96
|
-
unless challenge.fetch("digest") == reference.fetch("challenge_event_digest")
|
|
97
|
-
raise SecurityError, "cleanup inspection challenge differs"
|
|
98
|
-
end
|
|
99
|
-
Atoms::ProtectedWorkspacePruneInput.freeze_value(context.merge("challenge" => challenge))
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
private
|
|
103
|
-
|
|
104
|
-
def admit_original!(frame:, peer:, operation_owner_binding:, journal:, purpose:)
|
|
105
|
-
receiver!(peer)
|
|
106
|
-
Atoms::ProtectedWorkspacePruneInput.object!(frame, purpose == :execute ? FIELDS : INSPECT_FIELDS)
|
|
107
|
-
unless frame.values_at("schema", "kind") == [ProtectedCleanupOwnerClient::SCHEMA, purpose.to_s]
|
|
108
|
-
raise SecurityError, "cleanup invocation kind differs"
|
|
109
|
-
end
|
|
110
|
-
Atoms::ProtectedWorkspacePruneInput.token!(frame.fetch("request_id"))
|
|
111
|
-
(FIELDS - %w[schema kind request_id input]).each do |key|
|
|
112
|
-
Atoms::ProtectedWorkspacePruneInput.digest!(frame.fetch(key))
|
|
113
|
-
end
|
|
114
|
-
input = Atoms::ProtectedWorkspacePruneInput.parse(JSON.generate(frame.fetch("input")))
|
|
115
|
-
unless Atoms::ServiceInput.digest(input) == frame.fetch("input_digest") &&
|
|
116
|
-
(purpose == :inspect || digest(operation_owner_binding) == frame.fetch("operation_owner_binding_digest"))
|
|
117
|
-
raise SecurityError, "cleanup original input or owner differs"
|
|
118
|
-
end
|
|
119
|
-
maintenance = input.fetch("maintenance")
|
|
120
|
-
map = @deployment.mapping(maintenance.fetch("mapping_id"))
|
|
121
|
-
unless maintenance.fetch("mapping_id") == @mapping_id && map.fetch("authority_id") == @authority_id && map.fetch("project_id") == maintenance.fetch("project_id")
|
|
122
|
-
raise SecurityError, "cleanup installed scope differs"
|
|
123
|
-
end
|
|
124
|
-
unless journal.evidence_mode == :protected
|
|
125
|
-
raise SecurityError, "cleanup requires the installed protected service evidence owner"
|
|
126
|
-
end
|
|
127
|
-
commit = journal.ref_value
|
|
128
|
-
journal.verify_canonical_prefix!(commit: commit)
|
|
129
|
-
record = journal.service_request(frame.fetch("request_id"), commit: commit)
|
|
130
|
-
unless record && record.fetch("service_id") == @service_id && record.slice("project_id", "mapping_id", "assignment_id", "attempt_id") == maintenance &&
|
|
131
|
-
record.values_at("operation", "dispatch_phase") == ["prune-preserved-workspace", "dispatch_started"] &&
|
|
132
|
-
(purpose == :execute ? record["state"] == "uncertain" : %w[uncertain failed].include?(record["state"])) &&
|
|
133
|
-
record.slice("request_id", "input_digest", "claim_binding") == frame.slice("request_id", "input_digest", "claim_binding") &&
|
|
134
|
-
record.fetch("target") == Atoms::ServiceInput.target(input) &&
|
|
135
|
-
digest(record.fetch("operation_owner_binding")) == frame.fetch("operation_owner_binding_digest") &&
|
|
136
|
-
(purpose == :inspect || digest(record.fetch("executor_process_binding")) == digest(peer))
|
|
137
|
-
raise SecurityError, "cleanup canonical original dispatch differs"
|
|
138
|
-
end
|
|
139
|
-
Ace::Assign::Authority::ServiceEvidence.new(journal: journal).context(record, pending: {commit: commit})
|
|
140
|
-
receiver = @deployment.project(map.fetch("project_id")).fetch("service_receivers").fetch(record.fetch("service_id"))
|
|
141
|
-
credentials = @deployment.project(map.fetch("project_id")).fetch("peer_credentials").fetch(receiver.fetch("executor_uid").to_s)
|
|
142
|
-
unless peer.fetch("uid") == record.fetch("executor_uid") && peer.fetch("uid") == receiver.fetch("executor_uid") &&
|
|
143
|
-
peer.fetch("gid") == credentials.fetch("gid") && peer.fetch("groups") == credentials.fetch("groups")
|
|
144
|
-
raise SecurityError, "cleanup original executor differs"
|
|
145
|
-
end
|
|
146
|
-
@kernel.live!(peer)
|
|
147
|
-
proof = Ace::Assign::Authority::ServiceEvidence.new(journal: journal).cleanup_dispatch_context!(
|
|
148
|
-
record, commit: commit, selectors: frame.slice("request_event_digest", "dispatch_event_digest"))
|
|
149
|
-
if purpose == :execute && proof.fetch("dispatch_record") != record
|
|
150
|
-
raise SecurityError, "cleanup original executable record differs"
|
|
151
|
-
end
|
|
152
|
-
Atoms::ProtectedWorkspacePruneInput.freeze_value(JSON.parse(JSON.generate({"commit" => commit,
|
|
153
|
-
"record" => record, "input" => input, "operation_owner_binding" => operation_owner_binding,
|
|
154
|
-
"request_event_digest" => frame.fetch("request_event_digest"), "dispatch_event_digest" => frame.fetch("dispatch_event_digest")})))
|
|
155
|
-
rescue KeyError, TypeError, ArgumentError
|
|
156
|
-
raise SecurityError, "cleanup canonical admission is unavailable"
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def digest(value) = Ace::Assign::Atoms::EvidenceDigest.digest(value)
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
end
|