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,240 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "ace/support/cli"
|
|
5
|
-
require "ace/assign"
|
|
6
|
-
require_relative "support"
|
|
7
|
-
require_relative "../../organisms/protected_service_request"
|
|
8
|
-
require_relative "../../organisms/ordinary_merge_request"
|
|
9
|
-
require_relative "../../organisms/ordinary_merge_receiver"
|
|
10
|
-
require_relative "../../organisms/ordinary_authority_composition"
|
|
11
|
-
require_relative "../../molecules/ordinary_listener"
|
|
12
|
-
require_relative "../../molecules/ordinary_receiver_config"
|
|
13
|
-
|
|
14
|
-
module Ace
|
|
15
|
-
module Lab
|
|
16
|
-
module CLI
|
|
17
|
-
module Commands
|
|
18
|
-
module Service
|
|
19
|
-
module Output
|
|
20
|
-
def service
|
|
21
|
-
@service ||= Organisms::ServiceRequestService.new
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def protected_service
|
|
25
|
-
@protected_service ||= Organisms::ProtectedServiceRequest.new
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def protected_refusal(error)
|
|
29
|
-
code = (error.is_a?(ArgumentError) || error.is_a?(SecurityError)) ? "protected_service_refused" : "protected_service_unavailable"
|
|
30
|
-
message = error.is_a?(JSON::ParserError) ? "protected installed selection unavailable" : error.message.to_s.scrub[0, 512]
|
|
31
|
-
emit({"status" => "error", "error" => {"code" => code, "message" => message}})
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def emit(envelope)
|
|
35
|
-
puts JSON.generate(envelope)
|
|
36
|
-
return if envelope["status"] == "ok"
|
|
37
|
-
error = envelope.fetch("error")
|
|
38
|
-
code = error.fetch("code")
|
|
39
|
-
message = error.fetch("message")
|
|
40
|
-
raise Ace::Support::Cli::Error, "#{code}: #{message}"
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
class Request < Ace::Support::Cli::Command
|
|
45
|
-
include Ace::Support::Cli::Base
|
|
46
|
-
include Runtime
|
|
47
|
-
include Output
|
|
48
|
-
|
|
49
|
-
desc "Request one authorized, configured service operation"
|
|
50
|
-
option :project, type: :string, required: true, desc: "Project stable ID"
|
|
51
|
-
option :assignment, type: :string, required: true, desc: "Assignment ID"
|
|
52
|
-
option :attempt, type: :string, required: true, desc: "Managed attempt ID"
|
|
53
|
-
option :operation, type: :string, required: true, desc: "Named operation"
|
|
54
|
-
option :input, type: :string, required: true, desc: "Structured JSON input file"
|
|
55
|
-
option :authorization, type: :string, required: true, desc: "Exact authorization reference"
|
|
56
|
-
option :request_id, type: :string, required: true, desc: "Idempotency key"
|
|
57
|
-
option :dry_run, type: :boolean, desc: "Validate without claiming or executing"
|
|
58
|
-
option :mode, type: :string, desc: "ordinary-merge selects the ordinary fixed merge producer"
|
|
59
|
-
option :queue_entry, type: :string, desc: "Original queue entry (ordinary merge)"
|
|
60
|
-
|
|
61
|
-
option :mapping, type: :string, desc: "Original protected mapping ID"
|
|
62
|
-
option :scope, type: :string, desc: "Original hierarchical prepared scope"
|
|
63
|
-
option :service, type: :string, desc: "Installed receiver ID"
|
|
64
|
-
option :candidate_head, type: :string, desc: "Exact approved candidate head"
|
|
65
|
-
option :candidate_generation, type: :integer, desc: "Original candidate generation"
|
|
66
|
-
option :expected_generation, type: :integer, desc: "Original authority generation"
|
|
67
|
-
|
|
68
|
-
def call(project:, assignment:, attempt:, operation:, input:, authorization:, request_id:, **options)
|
|
69
|
-
reject_identity_flags!(options)
|
|
70
|
-
mode = options[:mode]
|
|
71
|
-
if mode && mode != Organisms::OrdinaryMergeRequest::MODE
|
|
72
|
-
return emit(Organisms::OrdinaryMergeRequest.refusal_envelope("ordinary_merge_invalid_request",
|
|
73
|
-
"unknown service mode", request_id: request_id, input_digest: nil))
|
|
74
|
-
end
|
|
75
|
-
if mode
|
|
76
|
-
begin
|
|
77
|
-
return emit(ordinary_request(project: project, assignment: assignment, attempt: attempt,
|
|
78
|
-
queue_entry: options[:queue_entry], operation: operation, input: input,
|
|
79
|
-
authorization: authorization, request_id: request_id, **options))
|
|
80
|
-
rescue Ace::Assign::AttemptErrors::EvidenceUnavailable, Ace::Lab::InvalidConfigurationError => e
|
|
81
|
-
# Infra failures (unreadable canonical evidence, malformed
|
|
82
|
-
# installed configuration, unavailable topology) are the
|
|
83
|
-
# caller-side blocker, never a raw crash and never a durable
|
|
84
|
-
# reference (review r1 finding B).
|
|
85
|
-
return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
|
|
86
|
-
"ordinary_merge_unavailable", e.message, request_id: request_id, input_digest: nil
|
|
87
|
-
))
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
selected = protected_service.selected?(options)
|
|
91
|
-
if selected
|
|
92
|
-
return emit(protected_service.request(project: project, assignment: assignment, attempt: attempt,
|
|
93
|
-
operation: operation, input_path: input, authorization: authorization, request_id: request_id, **options))
|
|
94
|
-
end
|
|
95
|
-
emit(service.request(project: project, assignment: assignment, attempt: attempt,
|
|
96
|
-
operation: operation, input_path: input, authorization: authorization,
|
|
97
|
-
request_id: request_id, dry_run: options[:dry_run]))
|
|
98
|
-
rescue ArgumentError, Ace::Assign::Error, Ace::Git::Error, Ace::Runtime::RuntimeUnavailableError, JSON::ParserError, SecurityError, KeyError, SystemCallError => error
|
|
99
|
-
raise if selected == false || mode
|
|
100
|
-
protected_refusal(error)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
private
|
|
104
|
-
|
|
105
|
-
def ordinary_producer
|
|
106
|
-
@ordinary_producer ||= Organisms::OrdinaryMergeRequest.new
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# The ordinary refusal envelope is emitted exactly as received;
|
|
110
|
-
# it is classified for the non-zero exit like every refusal.
|
|
111
|
-
def ordinary_request(**arguments)
|
|
112
|
-
ordinary_producer.request(**arguments)
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
class Status < Ace::Support::Cli::Command
|
|
117
|
-
include Ace::Support::Cli::Base
|
|
118
|
-
include Runtime
|
|
119
|
-
include Output
|
|
120
|
-
|
|
121
|
-
desc "Read the authoritative outcome of a service request"
|
|
122
|
-
option :request, type: :string, required: true, desc: "Request ID"
|
|
123
|
-
option :format, type: :string, desc: "Output format (json only)"
|
|
124
|
-
option :mode, type: :string, desc: "ordinary-merge selects the read-only ordinary status"
|
|
125
|
-
option :reference, type: :string, desc: "Opaque ordinary merge reference"
|
|
126
|
-
option :queue_entry, type: :string, desc: "Original queue entry (ordinary merge)"
|
|
127
|
-
|
|
128
|
-
option :project, type: :string, desc: "Original project ID"
|
|
129
|
-
option :assignment, type: :string, desc: "Original assignment ID"
|
|
130
|
-
option :attempt, type: :string, desc: "Original attempt ID"
|
|
131
|
-
option :mapping, type: :string, desc: "Original protected mapping ID"
|
|
132
|
-
option :scope, type: :string, desc: "Original hierarchical prepared scope"
|
|
133
|
-
option :candidate_head, type: :string, desc: "Exact approved candidate head"
|
|
134
|
-
option :candidate_generation, type: :integer, desc: "Original candidate generation"
|
|
135
|
-
option :input_digest, type: :string, desc: "Original normalized input SHA-256"
|
|
136
|
-
option :target, type: :string, desc: "Original target resource ID"
|
|
137
|
-
option :artifact_digest, type: :string, desc: "Original artifact SHA-256 (omit for null)"
|
|
138
|
-
|
|
139
|
-
def call(request:, **options)
|
|
140
|
-
reject_identity_flags!(options)
|
|
141
|
-
mode = options[:mode]
|
|
142
|
-
if mode && mode != Organisms::OrdinaryMergeRequest::MODE
|
|
143
|
-
return emit(Organisms::OrdinaryMergeRequest.refusal_envelope("ordinary_merge_invalid_request",
|
|
144
|
-
"unknown service mode", request_id: request, input_digest: nil,
|
|
145
|
-
schema: Ace::Assign::Atoms::FixedMergeSchemas::STATUS_ERROR_SCHEMA_ID))
|
|
146
|
-
end
|
|
147
|
-
if mode
|
|
148
|
-
ensure_json_format!(options)
|
|
149
|
-
begin
|
|
150
|
-
return emit(ordinary_producer.status(project: options[:project], assignment: options[:assignment],
|
|
151
|
-
attempt: options[:attempt], queue_entry: options[:queue_entry], reference: options[:reference],
|
|
152
|
-
request: request, input_digest: options[:input_digest], **options))
|
|
153
|
-
rescue Ace::Assign::AttemptErrors::EvidenceUnavailable, Ace::Lab::InvalidConfigurationError => e
|
|
154
|
-
return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
|
|
155
|
-
"ordinary_merge_unavailable", e.message, request_id: request, input_digest: nil,
|
|
156
|
-
schema: Ace::Assign::Atoms::FixedMergeSchemas::STATUS_ERROR_SCHEMA_ID
|
|
157
|
-
))
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
selected = protected_service.selected?(options)
|
|
161
|
-
if selected
|
|
162
|
-
return emit(protected_service.status(request_id: request, **options))
|
|
163
|
-
end
|
|
164
|
-
emit(service.status(request_id: request))
|
|
165
|
-
rescue ArgumentError, Ace::Assign::Error, Ace::Git::Error, Ace::Runtime::RuntimeUnavailableError, JSON::ParserError, SecurityError, KeyError, SystemCallError => error
|
|
166
|
-
raise if selected == false || mode
|
|
167
|
-
protected_refusal(error)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
private
|
|
171
|
-
|
|
172
|
-
def ordinary_producer
|
|
173
|
-
@ordinary_producer ||= Organisms::OrdinaryMergeRequest.new
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
# The ordinary receiver serve entrypoint (spec 8x3.t.xz9.4
|
|
178
|
-
# execution clauses; 8x3.t.xz9.5 integration handoff): binds the
|
|
179
|
-
# configured unix socket, mutually authenticates every connection
|
|
180
|
-
# through OS peer credentials, admits through the canonical Assign
|
|
181
|
-
# coordinator, executes the authorized fixed operation — one REAL
|
|
182
|
-
# Forgejo merge under the scoped host-installed service identity —
|
|
183
|
-
# and settles the exact terminal receipt proven by real forge
|
|
184
|
-
# readback. Configuration is one explicit deployment-owned file
|
|
185
|
-
# plus the host-installed authorization and forge-merge documents;
|
|
186
|
-
# the host owns their placement and credentials. Blocks until
|
|
187
|
-
# stopped.
|
|
188
|
-
class Listen < Ace::Support::Cli::Command
|
|
189
|
-
include Ace::Support::Cli::Base
|
|
190
|
-
include Output
|
|
191
|
-
|
|
192
|
-
desc "Serve ordinary Assign, HITL and fixed merge under the installed journal owner"
|
|
193
|
-
option :mode, type: :string, required: true,
|
|
194
|
-
desc: "ordinary-merge selects the ordinary fixed merge receiver"
|
|
195
|
-
option :project, type: :string, required: true, desc: "Project stable ID"
|
|
196
|
-
option :config, type: :string, required: true,
|
|
197
|
-
desc: "Deployment-owned receiver configuration file (YAML)"
|
|
198
|
-
|
|
199
|
-
def call(mode:, project:, config:, **options)
|
|
200
|
-
unless options.empty?
|
|
201
|
-
return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
|
|
202
|
-
"ordinary_merge_invalid_request",
|
|
203
|
-
"unknown listen selector: #{options.keys.sort.join(", ")}",
|
|
204
|
-
request_id: "unknown", input_digest: nil
|
|
205
|
-
))
|
|
206
|
-
end
|
|
207
|
-
unless mode == Organisms::OrdinaryMergeRequest::MODE
|
|
208
|
-
return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
|
|
209
|
-
"ordinary_merge_invalid_request", "unknown service mode",
|
|
210
|
-
request_id: "unknown", input_digest: nil
|
|
211
|
-
))
|
|
212
|
-
end
|
|
213
|
-
receiver_config = Molecules::OrdinaryReceiverConfig.load(config)
|
|
214
|
-
unless receiver_config.project == project
|
|
215
|
-
return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
|
|
216
|
-
"ordinary_merge_selection_mismatch",
|
|
217
|
-
"configured project does not match the requested project",
|
|
218
|
-
request_id: "unknown", input_digest: nil
|
|
219
|
-
))
|
|
220
|
-
end
|
|
221
|
-
unless receiver_config.receiver_uid == Process.uid
|
|
222
|
-
raise Ace::Lab::InvalidConfigurationError, "receiver must run as its configured UID"
|
|
223
|
-
end
|
|
224
|
-
server = Organisms::OrdinaryAuthorityComposition.new(receiver_config: receiver_config).build
|
|
225
|
-
signals = %w[INT TERM].to_h { |signal| [signal, Signal.trap(signal) { server.request_stop }] }
|
|
226
|
-
server.serve
|
|
227
|
-
rescue Ace::Lab::InvalidConfigurationError, Ace::Assign::Error, Ace::Runtime::Error, Ace::Hitl::Lifecycle::Error => e
|
|
228
|
-
emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
|
|
229
|
-
"ordinary_merge_unavailable", e.message,
|
|
230
|
-
request_id: "unknown", input_digest: nil
|
|
231
|
-
))
|
|
232
|
-
ensure
|
|
233
|
-
signals&.each { |signal, handler| Signal.trap(signal, handler) }
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Lab
|
|
5
|
-
module Models
|
|
6
|
-
# Non-secret result submitted to the assignment evidence coordinator.
|
|
7
|
-
# The coordinator independently verifies every binding before acceptance.
|
|
8
|
-
module ServiceReceipt
|
|
9
|
-
BINDING_FIELDS = %w[request_id assignment_id attempt_id project_id operation
|
|
10
|
-
input_digest target candidate_head executor_uid transport].freeze
|
|
11
|
-
|
|
12
|
-
def self.build(binding, executor_result)
|
|
13
|
-
BINDING_FIELDS.to_h { |field| [field, binding.fetch(field)] }.merge(executor_result)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "etc"
|
|
4
|
-
|
|
5
|
-
module Ace
|
|
6
|
-
module Lab
|
|
7
|
-
module Molecules
|
|
8
|
-
# The lab-side HITL delivery authorization (spec 8wq.t.34i): which
|
|
9
|
-
# authenticated transport may submit correlated answers for a
|
|
10
|
-
# Captain, derived from the SAME trusted deployment document that
|
|
11
|
-
# carries the service principals — read through the grant
|
|
12
|
-
# resolver's verified traversal, never from the cascade. The peer
|
|
13
|
-
# uid is a KERNEL fact resolved by the HITL boundary; this policy
|
|
14
|
-
# never trusts a claimed name.
|
|
15
|
-
class HitlAuthorizer
|
|
16
|
-
# @param principals [Hash] authorization.principals from the
|
|
17
|
-
# trusted document (uid-string or username keys, per-project)
|
|
18
|
-
# @param transport_uids [Array<Integer>] uids authorized to act
|
|
19
|
-
# as the configured transport
|
|
20
|
-
# @param service_uid [Integer, nil] the trusted HITL service uid
|
|
21
|
-
def initialize(principals: {}, transport_uids: [], service_uid: nil)
|
|
22
|
-
@principals = principals || {}
|
|
23
|
-
@transport_uids = Array(transport_uids)
|
|
24
|
-
@service_uid = service_uid
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Build from the trusted deployment document at a fixed path
|
|
28
|
-
# (same file the service policy resolves principals from).
|
|
29
|
-
# @return [HitlAuthorizer]
|
|
30
|
-
def self.from_trusted_document(path, loader = GrantResolver)
|
|
31
|
-
document = loader.trusted_document(path)
|
|
32
|
-
hitl = document["hitl"].is_a?(Hash) ? document["hitl"] : {}
|
|
33
|
-
principals = document.dig("authorization", "principals")
|
|
34
|
-
new(
|
|
35
|
-
principals: principals.is_a?(Hash) ? principals : {},
|
|
36
|
-
transport_uids: Array(hitl["transport_uids"]),
|
|
37
|
-
service_uid: hitl["service_uid"]
|
|
38
|
-
)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# @return [Integer, nil] the trusted HITL service identity
|
|
42
|
-
def service_uid
|
|
43
|
-
@service_uid.is_a?(Integer) ? @service_uid : nil
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# The transport may submit a correlated answer from an authorized
|
|
47
|
-
# Captain when its KERNEL-VERIFIED uid is a configured transport
|
|
48
|
-
# AND the transport's principal is visible for the request's
|
|
49
|
-
# project. Unknown identity is an error-shaped refusal, never
|
|
50
|
-
# permission.
|
|
51
|
-
def delivery_authorized?(uid:, project_id:)
|
|
52
|
-
return false unless @transport_uids.include?(uid)
|
|
53
|
-
|
|
54
|
-
policy = @principals[uid.to_s]
|
|
55
|
-
return false unless policy.is_a?(Hash)
|
|
56
|
-
|
|
57
|
-
projects = Array(policy["projects"]).map(&:to_s)
|
|
58
|
-
projects.empty? || projects.include?(project_id.to_s)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "time"
|
|
4
|
-
require_relative "service_policy"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Lab
|
|
8
|
-
module Molecules
|
|
9
|
-
# The scoped ordinary-merge decision/admission resolver (spec 8x3.t.xz9.4,
|
|
10
|
-
# execution clauses). The deployment-owned YAML authorization file remains
|
|
11
|
-
# the sole source of truth; this resolver only makes the exact root
|
|
12
|
-
# authorizations natively executable inside the receiver: one per-request
|
|
13
|
-
# authorization selector resolves to a typed decision within the closed
|
|
14
|
-
# refusal-code family — or it refuses. Scoping rules:
|
|
15
|
-
#
|
|
16
|
-
# - only `operation: merge` decisions of the ordinary shape resolve;
|
|
17
|
-
# anything else (missing reference, shape mismatch, non-merge
|
|
18
|
-
# operation, any binding mismatch, expiry) is a typed
|
|
19
|
-
# `ordinary_merge_authorization_invalid` decision;
|
|
20
|
-
# - exact binding match on every closed key; there are NO wildcards,
|
|
21
|
-
# defaults, prefix grants or broad grants of any kind — a decision
|
|
22
|
-
# authorizes exactly one bound request and nothing else;
|
|
23
|
-
# - expiry bounds the decision itself.
|
|
24
|
-
#
|
|
25
|
-
# Per-request refresh (spec 8x3.t.xz9.5 integration handoff): a
|
|
26
|
-
# long-lived receiver would otherwise decide every sequential candidate
|
|
27
|
-
# against its boot-time snapshot of the authorization document. When a
|
|
28
|
-
# trusted path is installed, every decision re-reads the document
|
|
29
|
-
# through the trusted loader and resolves against the FRESH grants —
|
|
30
|
-
# a root-revoked or expired decision fails closed at the next request
|
|
31
|
-
# without any receiver restart. The refresh is scoped: it reloads and
|
|
32
|
-
# re-validates the exact-request grant only; it never broadens
|
|
33
|
-
# authorization beyond the resolved reference.
|
|
34
|
-
class OrdinaryAdmissionResolver
|
|
35
|
-
BINDING_KEYS = ServicePolicy::ORDINARY_BINDING_KEYS.freeze
|
|
36
|
-
DECISION_KEYS = ServicePolicy::ORDINARY_AUTHORIZATION_KEYS.freeze
|
|
37
|
-
OPERATION = "merge"
|
|
38
|
-
|
|
39
|
-
Decision = Struct.new(:authorized?, :decision, :code, :message, keyword_init: true)
|
|
40
|
-
|
|
41
|
-
# @param policy_document [Hash] boot-time trusted document (the
|
|
42
|
-
# configuration digest is derived from it once, upstream)
|
|
43
|
-
# @param path [String, nil] trusted policy path to re-read per decision
|
|
44
|
-
# @param loader [Proc, nil] trusted reader (default: GrantResolver);
|
|
45
|
-
# the only override point, for non-privileged test documents
|
|
46
|
-
def initialize(policy_document, path: nil, loader: nil, proposal_resolver: nil, caller_uid: nil)
|
|
47
|
-
unless policy_document.is_a?(Hash) && policy_document["authorizations"].is_a?(Hash)
|
|
48
|
-
raise Ace::Lab::InvalidConfigurationError,
|
|
49
|
-
"trusted policy document must contain an authorizations mapping"
|
|
50
|
-
end
|
|
51
|
-
@authorizations = policy_document.fetch("authorizations")
|
|
52
|
-
@configuration_digest = ServicePolicy.configuration_digest(policy_document)
|
|
53
|
-
@path = path
|
|
54
|
-
@proposal_resolver = proposal_resolver
|
|
55
|
-
@caller_uid = caller_uid
|
|
56
|
-
@loader = loader || ->(trusted_path) { GrantResolver.trusted_document(trusted_path) }
|
|
57
|
-
freeze
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Resolve one authorization selector against the exact ordinary merge
|
|
61
|
-
# request binding. Returns a typed Decision; `authorized?` is true only
|
|
62
|
-
# for an exact, unexpired, ordinary-merge-shaped match.
|
|
63
|
-
def decide(reference, binding)
|
|
64
|
-
authorizations = current_authorizations
|
|
65
|
-
unless reference.is_a?(String) && reference.match?(ServicePolicy::ID)
|
|
66
|
-
return refuse("invalid authorization reference")
|
|
67
|
-
end
|
|
68
|
-
unless binding.is_a?(Hash) && binding.fetch("operation", nil) == OPERATION
|
|
69
|
-
return refuse("only the ordinary merge operation is resolvable")
|
|
70
|
-
end
|
|
71
|
-
if reference.start_with?("proposal-")
|
|
72
|
-
return refuse("canonical proposal authority is unavailable") unless @proposal_resolver && @caller_uid.is_a?(Integer)
|
|
73
|
-
begin
|
|
74
|
-
decision = @proposal_resolver.call(reference, binding.slice(
|
|
75
|
-
"operation", "project_id", "assignment_id", "attempt_id", "input_digest", "candidate_head").merge(
|
|
76
|
-
"target" => {"resource" => binding.fetch("pr_url"), "artifact_digest" => nil},
|
|
77
|
-
"caller_uid" => @caller_uid))
|
|
78
|
-
return Decision.new(authorized?: true, decision: decision, code: nil, message: nil)
|
|
79
|
-
rescue Ace::Assign::Error
|
|
80
|
-
return refuse("canonical proposal does not authorize this exact ordinary merge")
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
decision = authorizations[reference]
|
|
84
|
-
return refuse("authorization reference is unresolved") unless decision.is_a?(Hash)
|
|
85
|
-
unless DECISION_KEYS.all? { |key| decision.key?(key) }
|
|
86
|
-
return refuse("authorization reference is not an ordinary merge decision")
|
|
87
|
-
end
|
|
88
|
-
unless decision.fetch("operation") == OPERATION
|
|
89
|
-
return refuse("authorization does not cover the ordinary merge operation")
|
|
90
|
-
end
|
|
91
|
-
mismatch = BINDING_KEYS.find { |key| decision.fetch(key) != binding.fetch(key, nil) }
|
|
92
|
-
return refuse("authorization does not match the exact ordinary merge request") if mismatch
|
|
93
|
-
begin
|
|
94
|
-
expired = Time.iso8601(decision.fetch("expires_at").to_s) <= Time.now.utc
|
|
95
|
-
rescue ArgumentError
|
|
96
|
-
return refuse("authorization decision has an invalid expiry")
|
|
97
|
-
end
|
|
98
|
-
return refuse("authorization has expired") if expired
|
|
99
|
-
Decision.new(authorized?: true, decision: decision, code: nil, message: nil)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
private
|
|
103
|
-
|
|
104
|
-
# The authorizations mapping the CURRENT decision resolves against:
|
|
105
|
-
# the boot snapshot when no trusted path is installed, otherwise the
|
|
106
|
-
# freshly re-read document. A failed trusted re-read fails closed
|
|
107
|
-
# (InvalidConfigurationError — the unavailable family), never falls
|
|
108
|
-
# back to the stale snapshot.
|
|
109
|
-
def current_authorizations
|
|
110
|
-
return @authorizations unless @path
|
|
111
|
-
document = @loader.call(@path)
|
|
112
|
-
unless document.is_a?(Hash) && document["authorizations"].is_a?(Hash)
|
|
113
|
-
raise Ace::Lab::InvalidConfigurationError,
|
|
114
|
-
"invalid lab configuration: trusted authorization refresh at #{@path} " \
|
|
115
|
-
"did not yield an authorizations mapping"
|
|
116
|
-
end
|
|
117
|
-
unless ServicePolicy.configuration_digest(document) == @configuration_digest
|
|
118
|
-
raise Ace::Lab::InvalidConfigurationError,
|
|
119
|
-
"ordinary receiver configuration changed; restart with the installed configuration"
|
|
120
|
-
end
|
|
121
|
-
document.fetch("authorizations")
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def refuse(message)
|
|
125
|
-
Decision.new(authorized?: false, decision: nil,
|
|
126
|
-
code: "ordinary_merge_authorization_invalid", message: message)
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|