gitlab-glaz 2.0.0-x86_64-linux-gnu → 2.2.0-x86_64-linux-gnu
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/Cargo.lock +663 -598
- data/Cargo.toml +5 -5
- data/README.md +28 -5
- data/ext/glaz/src/lib.rs +79 -11
- data/lib/gitlab/glaz/3.1/glaz.so +0 -0
- data/lib/gitlab/glaz/3.2/glaz.so +0 -0
- data/lib/gitlab/glaz/3.3/glaz.so +0 -0
- data/lib/gitlab/glaz/3.4/glaz.so +0 -0
- data/lib/gitlab/glaz/4.0/glaz.so +0 -0
- data/lib/gitlab/glaz/govern_policy_engine.rb +344 -53
- data/lib/gitlab/glaz/version.rb +1 -1
- data/lib/gitlab/glaz.rb +7 -2
- data/lib/proto/govern_pb.rb +28 -20
- metadata +2 -2
|
@@ -9,20 +9,22 @@ module Gitlab
|
|
|
9
9
|
# policy evaluator.
|
|
10
10
|
#
|
|
11
11
|
# Evaluation is trigger-keyed: the caller supplies an event trigger and
|
|
12
|
-
# the context document
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
12
|
+
# the context document. The policies come either with the call
|
|
13
|
+
# (+policies:+ on #evaluate, for a caller that already has them, such as
|
|
14
|
+
# the Policy Store itself) or from the Policy Store REST API named at
|
|
15
|
+
# construction. Authentication for the REST API is per request
|
|
16
|
+
# (+store_authorization+ on #evaluate), never per engine and never from
|
|
17
|
+
# the environment. The engine executes no actions; the caller does.
|
|
17
18
|
#
|
|
18
19
|
# #validate and #debug_evaluate never touch the store, so an engine
|
|
19
|
-
# built without one
|
|
20
|
-
#
|
|
20
|
+
# built without one still serves them, and serves #evaluate when
|
|
21
|
+
# +policies:+ is given. An #evaluate call without +policies:+ on such an
|
|
22
|
+
# engine fails closed with an ArgumentError.
|
|
21
23
|
#
|
|
22
|
-
# #evaluate releases the GVL around the
|
|
23
|
-
# interrupted from Ruby until it returns, which is bounded by the
|
|
24
|
-
#
|
|
25
|
-
#
|
|
24
|
+
# #evaluate releases the GVL around the native call. It cannot be
|
|
25
|
+
# interrupted from Ruby until it returns, which is bounded by the lookup
|
|
26
|
+
# timeouts plus the batch evaluation. Instances are safe to share across
|
|
27
|
+
# threads and across a fork.
|
|
26
28
|
class GovernPolicyEngine
|
|
27
29
|
# Argument checks, raising ArgumentError instead of letting a
|
|
28
30
|
# Google::Protobuf::TypeError or JSON error escape from the wire layer.
|
|
@@ -81,6 +83,138 @@ module Gitlab
|
|
|
81
83
|
string.nil? || string.empty? ? nil : string
|
|
82
84
|
end
|
|
83
85
|
|
|
86
|
+
MODES = {
|
|
87
|
+
"enforce" => :GOVERN_ENFORCEMENT_MODE_ENFORCE,
|
|
88
|
+
"audit" => :GOVERN_ENFORCEMENT_MODE_AUDIT,
|
|
89
|
+
"warn" => :GOVERN_ENFORCEMENT_MODE_WARN
|
|
90
|
+
}.freeze
|
|
91
|
+
private_constant :MODES
|
|
92
|
+
|
|
93
|
+
# Builds the wire set from policy Hashes shaped like the Policy
|
|
94
|
+
# Store's policy entity. String or Symbol keys; keys the engine does
|
|
95
|
+
# not read, such as +lifecycle_state+, are ignored. Only the shape is
|
|
96
|
+
# checked here. Whether each policy belongs to the request's trigger
|
|
97
|
+
# and organization is the engine's check.
|
|
98
|
+
def policies!(policies)
|
|
99
|
+
return nil if policies.nil?
|
|
100
|
+
raise ArgumentError, "policies must be nil or an Array; got #{policies.class}" unless policies.is_a?(Array)
|
|
101
|
+
|
|
102
|
+
::Gitlab::Glaz::Govern::V1::GovernPolicySet.new(
|
|
103
|
+
policies: policies.each_with_index.map { |policy, index| policy!(policy, "policies[#{index}]") }
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def policy!(policy, name)
|
|
108
|
+
raise ArgumentError, "#{name} must be a Hash; got #{policy.class}" unless policy.is_a?(Hash)
|
|
109
|
+
|
|
110
|
+
policy = policy.transform_keys(&:to_s)
|
|
111
|
+
# Name the policy by id as well, as the native layer's errors do.
|
|
112
|
+
name = "#{name} (id #{policy['id']})" if policy["id"].is_a?(Integer)
|
|
113
|
+
dimensions, namespace_id = policy.values_at("scope_dimensions", "namespace_id")
|
|
114
|
+
|
|
115
|
+
attributes = {
|
|
116
|
+
id: integer!(policy["id"], "#{name}.id"),
|
|
117
|
+
name: optional_string!(policy["name"], "#{name}.name"),
|
|
118
|
+
trigger: string!(policy["trigger_type"], "#{name}.trigger_type"),
|
|
119
|
+
organization_id: integer!(policy["organization_id"], "#{name}.organization_id"),
|
|
120
|
+
mode: mode!(policy["mode"], "#{name}.mode"),
|
|
121
|
+
scope_rego: optional_string!(policy["scope_rego"], "#{name}.scope_rego"),
|
|
122
|
+
scope_dimensions: dimensions.nil? ? nil : scope_dimensions!(dimensions, "#{name}.scope_dimensions"),
|
|
123
|
+
policy_rego: string!(policy["policy_rego"], "#{name}.policy_rego"),
|
|
124
|
+
actions: actions!(policy["actions"], "#{name}.actions"),
|
|
125
|
+
violation_details: violation_details!(policy["violation_details"], "#{name}.violation_details")
|
|
126
|
+
}
|
|
127
|
+
# The owner is explicit on the wire: nil is an organization-level
|
|
128
|
+
# policy, never an unset field, so a missing key is an error rather
|
|
129
|
+
# than a silent default.
|
|
130
|
+
unless policy.key?("namespace_id")
|
|
131
|
+
raise ArgumentError, "#{name}.namespace_id must be present (nil for an organization-level policy)"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if namespace_id.nil?
|
|
135
|
+
attributes[:organization_level] = true
|
|
136
|
+
else
|
|
137
|
+
attributes[:namespace_id] = integer!(namespace_id, "#{name}.namespace_id")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
::Gitlab::Glaz::Govern::V1::GovernPolicy.new(attributes)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def integer!(value, name)
|
|
144
|
+
unless value.is_a?(Integer) && !value.negative? && value.bit_length <= 64
|
|
145
|
+
raise ArgumentError, "#{name} must be a non-negative Integer of at most 64 bits; got #{value.inspect}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
value
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def optional_string!(value, name)
|
|
152
|
+
value.nil? ? "" : string!(value, name)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def mode!(value, name)
|
|
156
|
+
MODES.fetch(value.to_s) do
|
|
157
|
+
raise ArgumentError, "#{name} must be one of #{MODES.keys.join(', ')}; got #{value.inspect}"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The engine stamps a mode on every decision, so an unspecified or
|
|
162
|
+
# unknown one is an engine fault, not a caller mistake.
|
|
163
|
+
def mode_name(value)
|
|
164
|
+
MODES.key(value) || raise("decision carries no enforcement mode; got #{value.inspect}")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def scope_dimensions!(value, name)
|
|
168
|
+
unless value.is_a?(Array) && value.all?(String)
|
|
169
|
+
raise ArgumentError, "#{name} must be nil or an Array of Strings; got #{value.inspect}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
::Gitlab::Glaz::Govern::V1::GovernScopeDimensions.new(paths: value)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# The store entity's saved constraints, forwarded onto the policy's
|
|
176
|
+
# decision as the store lookup forwards them: nil means none, a nil
|
|
177
|
+
# or missing +expected+ names no value, and other keys are ignored.
|
|
178
|
+
# The engine checks each path's segments.
|
|
179
|
+
def violation_details!(value, name)
|
|
180
|
+
return [] if value.nil?
|
|
181
|
+
raise ArgumentError, "#{name} must be nil or an Array; got #{value.class}" unless value.is_a?(Array)
|
|
182
|
+
|
|
183
|
+
value.each_with_index.map { |detail, index| violation_detail!(detail, "#{name}[#{index}]") }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def violation_detail!(detail, name)
|
|
187
|
+
raise ArgumentError, "#{name} must be a Hash; got #{detail.class}" unless detail.is_a?(Hash)
|
|
188
|
+
|
|
189
|
+
detail = detail.transform_keys(&:to_s)
|
|
190
|
+
expected = detail["expected"]
|
|
191
|
+
::Gitlab::Glaz::Govern::V1::GovernViolationDetail.new(
|
|
192
|
+
path: string!(detail["path"], "#{name}.path"),
|
|
193
|
+
expected_json: expected.nil? ? "" : JSON.generate(expected)
|
|
194
|
+
)
|
|
195
|
+
rescue JSON::JSONError => e
|
|
196
|
+
raise ArgumentError, "#{name}.expected must serialize to JSON; #{e.message}"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def actions!(value, name)
|
|
200
|
+
return [] if value.nil?
|
|
201
|
+
raise ArgumentError, "#{name} must be nil or an Array; got #{value.class}" unless value.is_a?(Array)
|
|
202
|
+
|
|
203
|
+
value.each_with_index.map { |action, index| action!(action, "#{name}[#{index}]") }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def action!(action, name)
|
|
207
|
+
raise ArgumentError, "#{name} must be a Hash; got #{action.class}" unless action.is_a?(Hash)
|
|
208
|
+
|
|
209
|
+
action = action.transform_keys(&:to_s)
|
|
210
|
+
value = action["value"]
|
|
211
|
+
|
|
212
|
+
::Gitlab::Glaz::Govern::V1::GovernPolicyAction.new(
|
|
213
|
+
action_type: string!(action["type"], "#{name}.type"),
|
|
214
|
+
value_json: value.nil? ? "" : json!(value, "#{name}.value")
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
|
|
84
218
|
def valid_store_base_url?(parsed_uri)
|
|
85
219
|
parsed_uri.is_a?(URI::HTTP) && !parsed_uri.host.to_s.empty? &&
|
|
86
220
|
parsed_uri.userinfo.nil? && parsed_uri.query.nil? && parsed_uri.fragment.nil? &&
|
|
@@ -172,24 +306,46 @@ module Gitlab
|
|
|
172
306
|
# @param store_authorization [String, nil] the `Authorization` header
|
|
173
307
|
# value the lookup presents to the Policy Store, verbatim (target
|
|
174
308
|
# scheme `"Bearer <jwt>"`). nil sends no header. Never logged or
|
|
175
|
-
# echoed in errors.
|
|
309
|
+
# echoed in errors. Ignored when +policies+ is given.
|
|
310
|
+
# @param policies [Array<Hash>, nil] the policies to evaluate, for a
|
|
311
|
+
# caller that already has them. When given, the engine evaluates
|
|
312
|
+
# exactly these and never contacts the store; an empty Array is a
|
|
313
|
+
# successful, empty evaluation. nil fetches them from the store.
|
|
314
|
+
# Pass the active policies of +trigger+ in the context's
|
|
315
|
+
# organization, each shaped like the Policy Store's policy entity
|
|
316
|
+
# (String or Symbol keys): `id`, `name`, `trigger_type`,
|
|
317
|
+
# `organization_id`, `namespace_id` (required; nil for an
|
|
318
|
+
# organization-level policy), `mode` (`"enforce"`, `"audit"` or
|
|
319
|
+
# `"warn"`), `scope_rego` (nil or empty when unscoped),
|
|
320
|
+
# `scope_dimensions` (Array<String>, or nil when unknown),
|
|
321
|
+
# `policy_rego`, `actions` (`{ type:, value: }` entries) and
|
|
322
|
+
# `violation_details` (nil, or `{ path:, expected: }` entries: the
|
|
323
|
+
# constraints the store saved from {#validate}, forwarded onto the
|
|
324
|
+
# policy's decision; `path` is a String and `expected`, nil when the
|
|
325
|
+
# policy names no compliant value, must serialize to JSON). Other
|
|
326
|
+
# keys are ignored.
|
|
176
327
|
# @raise [ArgumentError] for a caller fault: wrong argument types, a
|
|
177
328
|
# document that does not serialize to JSON, a blank trigger, a blank
|
|
178
329
|
# +principal+ or +resource+ (or one exceeding 64 KiB or carrying a
|
|
179
330
|
# newline), missing organization scope, an oversized input document,
|
|
180
|
-
#
|
|
331
|
+
# a policy in +policies+ that is malformed or belongs to another
|
|
332
|
+
# trigger, organization or namespace, more than the per-trigger cap
|
|
333
|
+
# of them, or an engine without a store called without +policies+.
|
|
181
334
|
# @raise [RuntimeError] for a backend fault: the store lookup failed
|
|
182
335
|
# or was rejected, or resolved more policies than the per-trigger
|
|
183
336
|
# cap. Never an empty batch, which would read as "nothing to
|
|
184
|
-
# enforce".
|
|
337
|
+
# enforce". Also when the engine stamps the response dry_run.
|
|
185
338
|
# @return [Hash] `{ decisions: Array<Hash>, undecided_policy_ids:
|
|
186
|
-
# Array<Integer>, identifier: String
|
|
339
|
+
# Array<Integer>, identifier: String,
|
|
340
|
+
# dry_run: Boolean }`. +decisions+ has one entry
|
|
187
341
|
# per resolved, in-scope policy, in lookup order; empty means
|
|
188
342
|
# nothing applies. Each decision is `{ policy_id: Integer, matched:
|
|
189
343
|
# Boolean, actions: Array<Hash>, reasons: Array<Hash>, error: String
|
|
190
|
-
# | nil
|
|
191
|
-
#
|
|
192
|
-
#
|
|
344
|
+
# | nil, name: String, mode: String, violation_details: Array<Hash> }`; each violation
|
|
345
|
+
# detail is `{ path: String, expected: Object }`, with +expected+ absent when the
|
|
346
|
+
# policy names no compliant value; each
|
|
347
|
+
# action `{ action_type: String, message: String | nil, params: Hash, gating: Boolean }`; each
|
|
348
|
+
# reason `{ message: String | nil, details: Hash }`. +actions+ are exactly those the
|
|
193
349
|
# store configured on the policy, emitted when it matched. A non-nil
|
|
194
350
|
# +error+ means the policy could not be decided (broken, or over the
|
|
195
351
|
# per-evaluation time budget); +matched+ is then false and its id is
|
|
@@ -201,34 +357,90 @@ module Gitlab
|
|
|
201
357
|
#
|
|
202
358
|
# Verdict: deny when any action is gating or any policy is
|
|
203
359
|
# undecided, allow otherwise. +matched+ alone decides nothing.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
360
|
+
# +dry_run+ is always false here; see #evaluate_dry_run to preview.
|
|
361
|
+
def evaluate(trigger:, context:, principal:, resource:, data: nil, store_authorization: nil, policies: nil)
|
|
362
|
+
request = evaluation_request(
|
|
363
|
+
trigger, context, principal, resource, data, store_authorization, policies, dry_run: false
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
response = ::Gitlab::Glaz::Govern::V1::EvaluateGovernPolicyResponse.decode(native.evaluate_policies(request))
|
|
367
|
+
# A dry-run stamp on an enforcement response would let a host skip
|
|
368
|
+
# recording a real decision, so treat it as an engine fault.
|
|
369
|
+
raise "enforcement evaluation response is stamped dry_run" if response.dry_run
|
|
370
|
+
|
|
371
|
+
evaluation_result(response)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Preview what #evaluate would decide, without enforcing it. A method
|
|
375
|
+
# of its own, never a parameter of #evaluate (GOVERN-004): a dry run is
|
|
376
|
+
# never a gate, so callers must not deny on its result, and nothing
|
|
377
|
+
# records it. Takes the same arguments, raises the same errors and
|
|
378
|
+
# returns the same Hash as #evaluate, with +dry_run+ true.
|
|
379
|
+
#
|
|
380
|
+
# @raise [RuntimeError] additionally, when the response does not echo
|
|
381
|
+
# the dry-run flag, so a dry run can never be mistaken for a verdict.
|
|
382
|
+
# @return [Hash] see #evaluate.
|
|
383
|
+
def evaluate_dry_run(
|
|
384
|
+
trigger:, context:, principal:, resource:, data: nil, store_authorization: nil,
|
|
385
|
+
policies: nil)
|
|
386
|
+
request = evaluation_request(
|
|
387
|
+
trigger, context, principal, resource, data, store_authorization, policies, dry_run: true
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
result = evaluation_result(
|
|
391
|
+
::Gitlab::Glaz::Govern::V1::EvaluateGovernPolicyResponse.decode(native.evaluate_policies_dry_run(request))
|
|
392
|
+
)
|
|
393
|
+
raise "dry-run evaluation response did not echo dry_run" unless result[:dry_run]
|
|
394
|
+
|
|
395
|
+
result
|
|
396
|
+
end
|
|
211
397
|
|
|
212
|
-
|
|
213
|
-
|
|
398
|
+
# List every policy that could apply to +trigger+ in the context's
|
|
399
|
+
# organization, each with whether it applies to the context supplied.
|
|
400
|
+
# A different question from #evaluate ("what must I enforce"): it needs
|
|
401
|
+
# only the trigger and the lookup keys (`organization.id`, plus the
|
|
402
|
+
# namespace ids where the trigger needs them), runs no policy's rules
|
|
403
|
+
# and returns no decisions, so nothing in it can be gated on.
|
|
404
|
+
#
|
|
405
|
+
# A policy whose scope reads a context path the caller did not supply
|
|
406
|
+
# is never evaluated, because a missing path would read as "does not
|
|
407
|
+
# apply". It comes back as `"might_apply"` with those paths in
|
|
408
|
+
# +missing_dimensions+: supply them and call again. An explicit nil in
|
|
409
|
+
# the context counts as absent; an empty Array is a value.
|
|
410
|
+
#
|
|
411
|
+
# @param trigger [String] the event, e.g. "deployment_requested".
|
|
412
|
+
# @param context [Hash] whatever context the caller has (max 1 MiB as
|
|
413
|
+
# JSON). Must carry `organization.id`, plus the namespace ids for a
|
|
414
|
+
# trigger whose policies belong to namespaces, as on #evaluate.
|
|
415
|
+
# @param store_authorization [String, nil] as on #evaluate.
|
|
416
|
+
# @param policies [Array<Hash>, nil] as on #evaluate.
|
|
417
|
+
# @raise [ArgumentError] for a caller fault, as on #evaluate (there is no
|
|
418
|
+
# +principal+ or +resource+ to check).
|
|
419
|
+
# @raise [RuntimeError] for a backend fault, as on #evaluate, or a row
|
|
420
|
+
# whose applicability the engine left unstated without saying why.
|
|
421
|
+
# @return [Hash] `{ policies: Array<Hash> }`, one entry per resolved
|
|
422
|
+
# policy in lookup order, each `{ policy_id: Integer, name: String,
|
|
423
|
+
# applicability: "applies" | "not_applicable" | "might_apply" | nil,
|
|
424
|
+
# missing_dimensions: Array<String>, error: String | nil }`.
|
|
425
|
+
# +missing_dimensions+ is set only with `"might_apply"`, and may be
|
|
426
|
+
# empty when the policy's scope paths are unknown. +applicability+
|
|
427
|
+
# is nil only alongside an +error+: the policy's scope could not be
|
|
428
|
+
# evaluated, so it is neither included nor ruled out. +name+ is for
|
|
429
|
+
# display; key on +policy_id+.
|
|
430
|
+
def list_policies(trigger:, context:, store_authorization: nil, policies: nil)
|
|
431
|
+
require_policy_source!(policies)
|
|
432
|
+
|
|
433
|
+
encoded = ::Gitlab::Glaz::Govern::V1::ListGovernPoliciesRequest.encode(
|
|
434
|
+
::Gitlab::Glaz::Govern::V1::ListGovernPoliciesRequest.new(
|
|
214
435
|
trigger: Validation.string!(trigger, "trigger"),
|
|
215
436
|
input_json: Validation.json!(context, "context"),
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
resource: Validation.string!(resource, "resource"),
|
|
219
|
-
store_authorization: Validation.store_authorization!(store_authorization)
|
|
437
|
+
store_authorization: Validation.store_authorization!(store_authorization),
|
|
438
|
+
policies: Validation.policies!(policies)
|
|
220
439
|
)
|
|
221
440
|
)
|
|
441
|
+
response = ::Gitlab::Glaz::Govern::V1::ListGovernPoliciesResponse.decode(native.list_policies(encoded))
|
|
222
442
|
|
|
223
|
-
response
|
|
224
|
-
native.evaluate_policies(encoded)
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
{
|
|
228
|
-
decisions: response.decisions.map { |decision| decision_hash(decision) },
|
|
229
|
-
undecided_policy_ids: response.undecided_policy_ids.to_a,
|
|
230
|
-
identifier: response.identifier
|
|
231
|
-
}
|
|
443
|
+
{ policies: response.policies.map { |row| applicability_hash(row) } }
|
|
232
444
|
end
|
|
233
445
|
|
|
234
446
|
# Parse and compile a Rego policy without evaluating it. A policy that
|
|
@@ -237,24 +449,32 @@ module Gitlab
|
|
|
237
449
|
# actionable feedback.
|
|
238
450
|
#
|
|
239
451
|
# @param policy_rego [String] Rego source (max 64 KiB).
|
|
240
|
-
# @
|
|
241
|
-
#
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
452
|
+
# @param trigger [String, nil] the trigger the policy is authored for. When
|
|
453
|
+
# given and the engine embeds a schema for it, every extracted detail's
|
|
454
|
+
# path and expected value are checked against that schema.
|
|
455
|
+
# @raise [ArgumentError] when +policy_rego+ or a given +trigger+ is not a String.
|
|
456
|
+
# @return [Hash] `{ valid: Boolean, errors: Array<Hash>,
|
|
457
|
+
# violation_details: Array<Hash> }`; each error is `{ message:
|
|
458
|
+
# String, location: String }` with +location+ in `"LINE:COL"`
|
|
459
|
+
# format, or `""` when none is available. +violation_details+ has
|
|
460
|
+
# the shape described on #evaluate and is empty for an invalid
|
|
461
|
+
# policy.
|
|
462
|
+
def validate(policy_rego:, trigger: nil)
|
|
463
|
+
encoded = ::Gitlab::Glaz::Govern::V1::ValidateGovernPolicyRequest.encode(
|
|
464
|
+
::Gitlab::Glaz::Govern::V1::ValidateGovernPolicyRequest.new(
|
|
465
|
+
policy_rego: Validation.string!(policy_rego, "policy_rego"),
|
|
466
|
+
trigger: trigger.nil? ? "" : Validation.string!(trigger, "trigger")
|
|
248
467
|
)
|
|
249
468
|
)
|
|
250
469
|
|
|
251
|
-
response = ::Glaz::Govern::V1::ValidateGovernPolicyResponse.decode(
|
|
470
|
+
response = ::Gitlab::Glaz::Govern::V1::ValidateGovernPolicyResponse.decode(
|
|
252
471
|
native.validate(encoded)
|
|
253
472
|
)
|
|
254
473
|
|
|
255
474
|
{
|
|
256
475
|
valid: response.valid,
|
|
257
|
-
errors: response.errors.map { |e| { message: e.message, location: e.location } }
|
|
476
|
+
errors: response.errors.map { |e| { message: e.message, location: e.location } },
|
|
477
|
+
violation_details: response.violation_details.map { |detail| violation_detail_hash(detail) }
|
|
258
478
|
}
|
|
259
479
|
end
|
|
260
480
|
|
|
@@ -275,8 +495,8 @@ module Gitlab
|
|
|
275
495
|
# whatever the query evaluated to (parsed JSON, String keys); nil
|
|
276
496
|
# when +error+ is set.
|
|
277
497
|
def debug_evaluate(policy_rego:, context:, query:, data: nil)
|
|
278
|
-
encoded = ::Glaz::Govern::V1::EvaluateGovernPolicyDebugRequest.encode(
|
|
279
|
-
::Glaz::Govern::V1::EvaluateGovernPolicyDebugRequest.new(
|
|
498
|
+
encoded = ::Gitlab::Glaz::Govern::V1::EvaluateGovernPolicyDebugRequest.encode(
|
|
499
|
+
::Gitlab::Glaz::Govern::V1::EvaluateGovernPolicyDebugRequest.new(
|
|
280
500
|
policy_rego: Validation.string!(policy_rego, "policy_rego"),
|
|
281
501
|
query: Validation.string!(query, "query"),
|
|
282
502
|
input_json: Validation.json!(context, "context"),
|
|
@@ -312,6 +532,41 @@ module Gitlab
|
|
|
312
532
|
@native
|
|
313
533
|
end
|
|
314
534
|
|
|
535
|
+
def require_policy_source!(policies)
|
|
536
|
+
return unless @store_base_url.nil? && policies.nil?
|
|
537
|
+
|
|
538
|
+
raise ArgumentError, "this engine has no Policy Store to resolve policies from: pass the policies " \
|
|
539
|
+
"as policies:, pass store_base_url: to Gitlab::Glaz::GovernPolicyEngine.new, or for the " \
|
|
540
|
+
"shared engine assign Gitlab::Glaz::GovernPolicyEngine.policy_store_url or set GLAZ_POLICY_STORE_URL " \
|
|
541
|
+
"(e.g. \"https://gitlab.com/api/v4\")"
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def evaluation_request(trigger, context, principal, resource, data, store_authorization, policies, dry_run:)
|
|
545
|
+
require_policy_source!(policies)
|
|
546
|
+
|
|
547
|
+
::Gitlab::Glaz::Govern::V1::EvaluateGovernPolicyRequest.encode(
|
|
548
|
+
::Gitlab::Glaz::Govern::V1::EvaluateGovernPolicyRequest.new(
|
|
549
|
+
trigger: Validation.string!(trigger, "trigger"),
|
|
550
|
+
input_json: Validation.json!(context, "context"),
|
|
551
|
+
data_json: data.nil? ? "" : Validation.json!(data, "data"),
|
|
552
|
+
principal: Validation.string!(principal, "principal"),
|
|
553
|
+
resource: Validation.string!(resource, "resource"),
|
|
554
|
+
store_authorization: Validation.store_authorization!(store_authorization),
|
|
555
|
+
policies: Validation.policies!(policies),
|
|
556
|
+
dry_run: dry_run
|
|
557
|
+
)
|
|
558
|
+
)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def evaluation_result(response)
|
|
562
|
+
{
|
|
563
|
+
decisions: response.decisions.map { |decision| decision_hash(decision) },
|
|
564
|
+
undecided_policy_ids: response.undecided_policy_ids.to_a,
|
|
565
|
+
identifier: response.identifier,
|
|
566
|
+
dry_run: response.dry_run
|
|
567
|
+
}
|
|
568
|
+
end
|
|
569
|
+
|
|
315
570
|
def build_native
|
|
316
571
|
::Glaz::Native::GovernPolicyEngine.new(@store_base_url, @connect_timeout_ms, @timeout_ms)
|
|
317
572
|
end
|
|
@@ -322,10 +577,46 @@ module Gitlab
|
|
|
322
577
|
matched: decision.matched,
|
|
323
578
|
actions: decision.actions.map { |action| action_hash(action) },
|
|
324
579
|
reasons: decision.reasons.map { |reason| reason_hash(reason) },
|
|
325
|
-
error: Validation.presence(decision.error)
|
|
580
|
+
error: Validation.presence(decision.error),
|
|
581
|
+
name: decision.name,
|
|
582
|
+
mode: Validation.mode_name(decision.mode),
|
|
583
|
+
violation_details: decision.violation_details.map { |detail| violation_detail_hash(detail) }
|
|
326
584
|
}
|
|
327
585
|
end
|
|
328
586
|
|
|
587
|
+
APPLICABILITIES = {
|
|
588
|
+
APPLICABILITY_APPLIES: "applies",
|
|
589
|
+
APPLICABILITY_NOT_APPLICABLE: "not_applicable",
|
|
590
|
+
APPLICABILITY_MIGHT_APPLY: "might_apply"
|
|
591
|
+
}.freeze
|
|
592
|
+
private_constant :APPLICABILITIES
|
|
593
|
+
|
|
594
|
+
# UNSPECIFIED is the engine's "not stated", which it sends only with an
|
|
595
|
+
# error. Without one it would read as an answer, so it is a fault.
|
|
596
|
+
def applicability_hash(row)
|
|
597
|
+
error = Validation.presence(row.error)
|
|
598
|
+
applicability = APPLICABILITIES[row.applicability]
|
|
599
|
+
if applicability.nil? && error.nil?
|
|
600
|
+
raise "policy #{row.policy_id} has no applicability and no error; got #{row.applicability.inspect}"
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
{
|
|
604
|
+
policy_id: row.policy_id,
|
|
605
|
+
name: row.name,
|
|
606
|
+
applicability: applicability,
|
|
607
|
+
missing_dimensions: row.missing_dimensions.to_a,
|
|
608
|
+
error: error
|
|
609
|
+
}
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# +expected_json+ may encode a scalar, which json 2 parses as a
|
|
613
|
+
# document; +quirks_mode+ only matters to older json versions.
|
|
614
|
+
def violation_detail_hash(detail)
|
|
615
|
+
return { path: detail.path } if detail.expected_json.empty?
|
|
616
|
+
|
|
617
|
+
{ path: detail.path, expected: JSON.parse(detail.expected_json, max_nesting: false, quirks_mode: true) }
|
|
618
|
+
end
|
|
619
|
+
|
|
329
620
|
# The native layer already bounds nesting, so parse without a limit.
|
|
330
621
|
def action_hash(action)
|
|
331
622
|
{
|
data/lib/gitlab/glaz/version.rb
CHANGED
data/lib/gitlab/glaz.rb
CHANGED
|
@@ -16,7 +16,12 @@ module Gitlab
|
|
|
16
16
|
|
|
17
17
|
# Returns the default roles embedded in the native extension at build time,
|
|
18
18
|
# sourced from the glaz-roles crate's roles/**/*.yml definitions. Each entry
|
|
19
|
-
# is a hash:
|
|
19
|
+
# is a hash:
|
|
20
|
+
# { id: String, key: String, name: String, permissions: Array<String> }.
|
|
21
|
+
#
|
|
22
|
+
# :key is the role's stable machine identifier (e.g. "artifact_admin"):
|
|
23
|
+
# reference a role by it rather than hardcoding :id or matching on :name,
|
|
24
|
+
# which is human-facing and may be reworded.
|
|
20
25
|
def roles
|
|
21
26
|
@roles ||= ::Glaz.roles.each do |role|
|
|
22
27
|
role[:permissions].freeze
|
|
@@ -27,7 +32,7 @@ module Gitlab
|
|
|
27
32
|
# Returns GovernPolicyEngine.shared, the engine configured through
|
|
28
33
|
# GovernPolicyEngine.policy_store_url= / GLAZ_POLICY_STORE_URL. It is
|
|
29
34
|
# rebuilt when that configuration changes, so call this each time rather
|
|
30
|
-
# than caching the result.
|
|
35
|
+
# than caching the result. Not needed when #evaluate gets +policies:+.
|
|
31
36
|
def govern_policy_engine
|
|
32
37
|
GovernPolicyEngine.shared
|
|
33
38
|
end
|
data/lib/proto/govern_pb.rb
CHANGED
|
@@ -1,33 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
-
# source:
|
|
3
|
+
# source: gitlab/glaz/govern/v1/govern.proto
|
|
4
4
|
|
|
5
5
|
require 'google/protobuf'
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
descriptor_data = "\n\
|
|
8
|
+
descriptor_data = "\n\"gitlab/glaz/govern/v1/govern.proto\x12\x15gitlab.glaz.govern.v1\"m\n EvaluateGovernPolicyDebugRequest\x12\x13\n\x0bpolicy_rego\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x12\n\ninput_json\x18\x03 \x01(\t\x12\x11\n\tdata_json\x18\x04 \x01(\t\"Y\n\x0cGovernAction\x12\x13\n\x0b\x61\x63tion_type\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x13\n\x0bparams_json\x18\x03 \x01(\t\x12\x0e\n\x06gating\x18\x04 \x01(\x08\"5\n\x0cGovernReason\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x14\n\x0c\x64\x65tails_json\x18\x02 \x01(\t\"C\n\x1bValidateGovernPolicyRequest\x12\x13\n\x0bpolicy_rego\x18\x01 \x01(\t\x12\x0f\n\x07trigger\x18\x02 \x01(\t\":\n\x15GovernValidationError\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x10\n\x08location\x18\x02 \x01(\t\"\xb4\x01\n\x1cValidateGovernPolicyResponse\x12\r\n\x05valid\x18\x01 \x01(\x08\x12<\n\x06\x65rrors\x18\x02 \x03(\x0b\x32,.gitlab.glaz.govern.v1.GovernValidationError\x12G\n\x11violation_details\x18\x03 \x03(\x0b\x32,.gitlab.glaz.govern.v1.GovernViolationDetail\"<\n\x15GovernViolationDetail\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x15\n\rexpected_json\x18\x02 \x01(\t\"=\n\x12GovernPolicyAction\x12\x13\n\x0b\x61\x63tion_type\x18\x01 \x01(\t\x12\x12\n\nvalue_json\x18\x02 \x01(\t\"&\n\x15GovernScopeDimensions\x12\r\n\x05paths\x18\x01 \x03(\t\"\xc3\x03\n\x0cGovernPolicy\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0f\n\x07trigger\x18\x03 \x01(\t\x12\x17\n\x0forganization_id\x18\x04 \x01(\x04\x12:\n\x04mode\x18\x05 \x01(\x0e\x32,.gitlab.glaz.govern.v1.GovernEnforcementMode\x12\x12\n\nscope_rego\x18\x06 \x01(\t\x12\x46\n\x10scope_dimensions\x18\x07 \x01(\x0b\x32,.gitlab.glaz.govern.v1.GovernScopeDimensions\x12\x13\n\x0bpolicy_rego\x18\x08 \x01(\t\x12:\n\x07\x61\x63tions\x18\t \x03(\x0b\x32).gitlab.glaz.govern.v1.GovernPolicyAction\x12\x16\n\x0cnamespace_id\x18\n \x01(\x04H\x00\x12\x1c\n\x12organization_level\x18\x0b \x01(\x08H\x00\x12G\n\x11violation_details\x18\x0c \x03(\x0b\x32,.gitlab.glaz.govern.v1.GovernViolationDetailB\x07\n\x05owner\"H\n\x0fGovernPolicySet\x12\x35\n\x08policies\x18\x01 \x03(\x0b\x32#.gitlab.glaz.govern.v1.GovernPolicy\"\xe2\x01\n\x1b\x45valuateGovernPolicyRequest\x12\x0f\n\x07trigger\x18\x01 \x01(\t\x12\x12\n\ninput_json\x18\x02 \x01(\t\x12\x11\n\tdata_json\x18\x03 \x01(\t\x12\x11\n\tprincipal\x18\x04 \x01(\t\x12\x10\n\x08resource\x18\x05 \x01(\t\x12\x1b\n\x13store_authorization\x18\x06 \x01(\t\x12\x38\n\x08policies\x18\x07 \x01(\x0b\x32&.gitlab.glaz.govern.v1.GovernPolicySet\x12\x0f\n\x07\x64ry_run\x18\x08 \x01(\x08\"\x97\x01\n\x19ListGovernPoliciesRequest\x12\x0f\n\x07trigger\x18\x01 \x01(\t\x12\x12\n\ninput_json\x18\x02 \x01(\t\x12\x1b\n\x13store_authorization\x18\x03 \x01(\t\x12\x38\n\x08policies\x18\x04 \x01(\x0b\x32&.gitlab.glaz.govern.v1.GovernPolicySet\"\x9e\x01\n\x13PolicyApplicability\x12\x11\n\tpolicy_id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\rapplicability\x18\x03 \x01(\x0e\x32$.gitlab.glaz.govern.v1.Applicability\x12\x1a\n\x12missing_dimensions\x18\x04 \x03(\t\x12\r\n\x05\x65rror\x18\x05 \x01(\t\"Z\n\x1aListGovernPoliciesResponse\x12<\n\x08policies\x18\x01 \x03(\x0b\x32*.gitlab.glaz.govern.v1.PolicyApplicability\"\xc2\x02\n\x0ePolicyDecision\x12\x11\n\tpolicy_id\x18\x01 \x01(\x04\x12\x0f\n\x07matched\x18\x02 \x01(\x08\x12\x34\n\x07\x61\x63tions\x18\x03 \x03(\x0b\x32#.gitlab.glaz.govern.v1.GovernAction\x12\x34\n\x07reasons\x18\x04 \x03(\x0b\x32#.gitlab.glaz.govern.v1.GovernReason\x12\r\n\x05\x65rror\x18\x05 \x01(\t\x12\x0c\n\x04name\x18\x06 \x01(\t\x12:\n\x04mode\x18\x07 \x01(\x0e\x32,.gitlab.glaz.govern.v1.GovernEnforcementMode\x12G\n\x11violation_details\x18\x08 \x03(\x0b\x32,.gitlab.glaz.govern.v1.GovernViolationDetail\"X\n\x10GovernDiagnostic\x12\x33\n\x04kind\x18\x01 \x01(\x0e\x32%.gitlab.glaz.govern.v1.DiagnosticKind\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xd9\x01\n\x1c\x45valuateGovernPolicyResponse\x12\x38\n\tdecisions\x18\x01 \x03(\x0b\x32%.gitlab.glaz.govern.v1.PolicyDecision\x12\x1c\n\x14undecided_policy_ids\x18\x02 \x03(\x04\x12<\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\'.gitlab.glaz.govern.v1.GovernDiagnostic\x12\x12\n\nidentifier\x18\x04 \x01(\t\x12\x0f\n\x07\x64ry_run\x18\x05 \x01(\x08*\xaa\x01\n\x15GovernEnforcementMode\x12\'\n#GOVERN_ENFORCEMENT_MODE_UNSPECIFIED\x10\x00\x12!\n\x1dGOVERN_ENFORCEMENT_MODE_AUDIT\x10\x01\x12 \n\x1cGOVERN_ENFORCEMENT_MODE_WARN\x10\x02\x12#\n\x1fGOVERN_ENFORCEMENT_MODE_ENFORCE\x10\x03*\x8a\x01\n\rApplicability\x12\x1d\n\x19\x41PPLICABILITY_UNSPECIFIED\x10\x00\x12\x19\n\x15\x41PPLICABILITY_APPLIES\x10\x01\x12 \n\x1c\x41PPLICABILITY_NOT_APPLICABLE\x10\x02\x12\x1d\n\x19\x41PPLICABILITY_MIGHT_APPLY\x10\x03*\x83\x01\n\x0e\x44iagnosticKind\x12\x1f\n\x1b\x44IAGNOSTIC_KIND_UNSPECIFIED\x10\x00\x12*\n&DIAGNOSTIC_KIND_INPUT_SCHEMA_VIOLATION\x10\x01\x12$\n DIAGNOSTIC_KIND_SCHEMA_NOT_FOUND\x10\x02\x62\x06proto3"
|
|
9
9
|
|
|
10
10
|
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
11
11
|
pool.add_serialized_file(descriptor_data)
|
|
12
12
|
|
|
13
|
-
module
|
|
14
|
-
module
|
|
15
|
-
module
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
13
|
+
module Gitlab
|
|
14
|
+
module Glaz
|
|
15
|
+
module Govern
|
|
16
|
+
module V1
|
|
17
|
+
EvaluateGovernPolicyDebugRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.EvaluateGovernPolicyDebugRequest").msgclass
|
|
18
|
+
GovernAction = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.GovernAction").msgclass
|
|
19
|
+
GovernReason = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.GovernReason").msgclass
|
|
20
|
+
ValidateGovernPolicyRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.ValidateGovernPolicyRequest").msgclass
|
|
21
|
+
GovernValidationError = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.GovernValidationError").msgclass
|
|
22
|
+
ValidateGovernPolicyResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.ValidateGovernPolicyResponse").msgclass
|
|
23
|
+
GovernViolationDetail = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.GovernViolationDetail").msgclass
|
|
24
|
+
GovernPolicyAction = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.GovernPolicyAction").msgclass
|
|
25
|
+
GovernScopeDimensions = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.GovernScopeDimensions").msgclass
|
|
26
|
+
GovernPolicy = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.GovernPolicy").msgclass
|
|
27
|
+
GovernPolicySet = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.GovernPolicySet").msgclass
|
|
28
|
+
EvaluateGovernPolicyRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.EvaluateGovernPolicyRequest").msgclass
|
|
29
|
+
ListGovernPoliciesRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.ListGovernPoliciesRequest").msgclass
|
|
30
|
+
PolicyApplicability = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.PolicyApplicability").msgclass
|
|
31
|
+
ListGovernPoliciesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.ListGovernPoliciesResponse").msgclass
|
|
32
|
+
PolicyDecision = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.PolicyDecision").msgclass
|
|
33
|
+
GovernDiagnostic = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.GovernDiagnostic").msgclass
|
|
34
|
+
EvaluateGovernPolicyResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.EvaluateGovernPolicyResponse").msgclass
|
|
35
|
+
GovernEnforcementMode = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.GovernEnforcementMode").enummodule
|
|
36
|
+
Applicability = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.Applicability").enummodule
|
|
37
|
+
DiagnosticKind = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitlab.glaz.govern.v1.DiagnosticKind").enummodule
|
|
38
|
+
end
|
|
31
39
|
end
|
|
32
40
|
end
|
|
33
41
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitlab-glaz
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: x86_64-linux-gnu
|
|
6
6
|
authors:
|
|
7
7
|
- group::authorization
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: google-protobuf
|