gitlab-glaz 1.1.0 → 2.0.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/Cargo.lock +682 -34
- data/Cargo.toml +4 -4
- data/README.md +25 -1
- data/ext/glaz/Cargo.toml +1 -0
- data/ext/glaz/src/lib.rs +241 -38
- data/lib/gitlab/glaz/govern_policy_engine.rb +295 -88
- data/lib/gitlab/glaz/version.rb +1 -1
- data/lib/gitlab/glaz.rb +5 -5
- data/lib/proto/govern_pb.rb +12 -2
- metadata +4 -4
|
@@ -1,138 +1,345 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "uri"
|
|
4
5
|
|
|
5
6
|
module Gitlab
|
|
6
7
|
module Glaz
|
|
7
|
-
# Wraps a native Glaz::Native::GovernPolicyEngine
|
|
8
|
-
#
|
|
8
|
+
# Wraps a native Glaz::Native::GovernPolicyEngine - the Rego governance
|
|
9
|
+
# policy evaluator.
|
|
9
10
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# Evaluation is trigger-keyed: the caller supplies an event trigger and
|
|
12
|
+
# the context document, and the engine fetches the applicable policies
|
|
13
|
+
# from the Policy Store REST API named at construction. Authentication
|
|
14
|
+
# is per request (+store_authorization+ on #evaluate), never per engine
|
|
15
|
+
# and never from the environment. The engine executes no actions; the
|
|
16
|
+
# caller does.
|
|
14
17
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# reports what to enforce as a result. Actions are conceptually owned by
|
|
19
|
-
# the policy's configuration in the future central Policy Store, so any
|
|
20
|
-
# match currently yields a single synthesized block action regardless of
|
|
21
|
-
# how many reasons fired. +matched+ is a convenience boolean mirroring
|
|
22
|
-
# "+actions+ is non-empty", for callers who don't want to infer that
|
|
23
|
-
# from array emptiness themselves.
|
|
18
|
+
# #validate and #debug_evaluate never touch the store, so an engine
|
|
19
|
+
# built without one (the Policy Store itself) still serves them and
|
|
20
|
+
# fails #evaluate closed with an ArgumentError.
|
|
24
21
|
#
|
|
25
|
-
# #evaluate
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# bad policy/input and RuntimeError for an engine-side fault; see
|
|
30
|
-
# #evaluate's own doc comment.
|
|
31
|
-
# #debug_evaluate is a separate, secondary method for debugging/testing a Rego query
|
|
32
|
-
# directly: it performs no violation/deny/allow interpretation, does
|
|
33
|
-
# not raise, and returns whatever the query evaluated to (plus any
|
|
34
|
-
# error, in-band) with no guarantee about its shape.
|
|
22
|
+
# #evaluate releases the GVL around the store lookup. It cannot be
|
|
23
|
+
# interrupted from Ruby until it returns, which is bounded by the
|
|
24
|
+
# lookup timeouts plus the batch evaluation. Instances are safe to
|
|
25
|
+
# share across threads and across a fork.
|
|
35
26
|
class GovernPolicyEngine
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
# Argument checks, raising ArgumentError instead of letting a
|
|
28
|
+
# Google::Protobuf::TypeError or JSON error escape from the wire layer.
|
|
29
|
+
#
|
|
30
|
+
# @api private
|
|
31
|
+
module Validation
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
INVALID_STORE_BASE_URL_MESSAGE = "store_base_url must be an http(s) URL naming the Policy Store REST API " \
|
|
35
|
+
"root (e.g. \"https://gitlab.com/api/v4\"), without credentials, query or fragment, and with a port " \
|
|
36
|
+
"in 1..65535"
|
|
37
|
+
private_constant :INVALID_STORE_BASE_URL_MESSAGE
|
|
38
|
+
|
|
39
|
+
def store_base_url!(url)
|
|
40
|
+
string!(url, "store_base_url")
|
|
41
|
+
|
|
42
|
+
parsed = begin
|
|
43
|
+
URI.parse(url)
|
|
44
|
+
rescue URI::Error
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
raise ArgumentError, INVALID_STORE_BASE_URL_MESSAGE unless valid_store_base_url?(parsed)
|
|
48
|
+
|
|
49
|
+
-url
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def string!(value, name)
|
|
53
|
+
raise ArgumentError, "#{name} must be a String; got #{value.class}" unless value.is_a?(String)
|
|
54
|
+
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The message never echoes +value+: it is a credential.
|
|
59
|
+
def store_authorization!(value)
|
|
60
|
+
return value.to_s if value.nil? || value.is_a?(String)
|
|
61
|
+
|
|
62
|
+
raise ArgumentError, "store_authorization must be nil or a String; got a #{value.class}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The native side takes a u64 of milliseconds.
|
|
66
|
+
def timeout!(value, name)
|
|
67
|
+
return value if value.nil? || (value.is_a?(Integer) && value.positive? && value.bit_length <= 64)
|
|
68
|
+
|
|
69
|
+
raise ArgumentError, "#{name} must be nil or a positive Integer of at most 64 bits; got #{value.inspect}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def json!(document, name)
|
|
73
|
+
raise ArgumentError, "#{name} must be a Hash; got #{document.class}" unless document.is_a?(Hash)
|
|
74
|
+
|
|
75
|
+
JSON.generate(document)
|
|
76
|
+
rescue JSON::JSONError => e
|
|
77
|
+
raise ArgumentError, "#{name} must serialize to JSON; #{e.message}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def presence(string)
|
|
81
|
+
string.nil? || string.empty? ? nil : string
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def valid_store_base_url?(parsed_uri)
|
|
85
|
+
parsed_uri.is_a?(URI::HTTP) && !parsed_uri.host.to_s.empty? &&
|
|
86
|
+
parsed_uri.userinfo.nil? && parsed_uri.query.nil? && parsed_uri.fragment.nil? &&
|
|
87
|
+
parsed_uri.port.between?(1, 65_535)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
private_constant :Validation
|
|
91
|
+
|
|
92
|
+
# Serializes .shared against .policy_store_url=: without it, a .shared
|
|
93
|
+
# call that read the old URL could finish after a reconfiguration and
|
|
94
|
+
# leave an engine for the old store memoized.
|
|
95
|
+
CONFIGURATION_LOCK = Mutex.new
|
|
96
|
+
private_constant :CONFIGURATION_LOCK
|
|
97
|
+
|
|
98
|
+
class << self
|
|
99
|
+
# @return [GovernPolicyEngine] a process-wide engine for
|
|
100
|
+
# +.policy_store_url+, built on first access and memoized until
|
|
101
|
+
# +.policy_store_url=+ next reconfigures it.
|
|
102
|
+
# @raise [ArgumentError] if +GLAZ_POLICY_STORE_URL+ is set but is not
|
|
103
|
+
# a valid Policy Store REST API root.
|
|
104
|
+
def shared
|
|
105
|
+
CONFIGURATION_LOCK.synchronize { @shared ||= new(store_base_url: policy_store_url) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @return [String, nil] the Policy Store REST API root the next
|
|
109
|
+
# +.shared+ call builds its engine with: an explicitly assigned URL
|
|
110
|
+
# (see +.policy_store_url=+) takes precedence, otherwise
|
|
111
|
+
# +GLAZ_POLICY_STORE_URL+; a blank value counts as unset (+nil+).
|
|
112
|
+
def policy_store_url
|
|
113
|
+
@policy_store_url || Validation.presence(ENV["GLAZ_POLICY_STORE_URL"]&.strip)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Point the shared engine at a Policy Store REST API root; the next
|
|
117
|
+
# .shared call rebuilds it. +nil+ falls back to the environment variable.
|
|
118
|
+
#
|
|
119
|
+
# @raise [ArgumentError] when +url+ is neither nil nor an http(s) URL
|
|
120
|
+
# without credentials, query or fragment.
|
|
121
|
+
def policy_store_url=(url)
|
|
122
|
+
validated = url.nil? ? nil : Validation.store_base_url!(url)
|
|
123
|
+
CONFIGURATION_LOCK.synchronize do
|
|
124
|
+
@policy_store_url = validated
|
|
125
|
+
@shared = nil
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @return [String, nil] the Policy Store REST API root; nil for an
|
|
131
|
+
# engine with no store.
|
|
132
|
+
attr_reader :store_base_url
|
|
133
|
+
|
|
134
|
+
# @param store_base_url [String, nil] the Policy Store REST API root,
|
|
135
|
+
# e.g. `"https://gitlab.com/api/v4"`; the lookup performs
|
|
136
|
+
# `GET {store_base_url}/organizations/{id}/security/policy_store?trigger_type={trigger}`.
|
|
137
|
+
# Redirects are never followed; `ALL_PROXY`/`HTTPS_PROXY`/`HTTP_PROXY`
|
|
138
|
+
# and `NO_PROXY` apply. +nil+ builds an engine with no store.
|
|
139
|
+
# @param connect_timeout_ms [Integer, nil] lookup connect timeout
|
|
140
|
+
# (default 500 ms).
|
|
141
|
+
# @param timeout_ms [Integer, nil] lookup total timeout (default 2 s).
|
|
142
|
+
# @raise [ArgumentError] for an invalid URL, or a timeout that is
|
|
143
|
+
# neither nil nor a positive 64-bit Integer.
|
|
144
|
+
def initialize(store_base_url: nil, connect_timeout_ms: nil, timeout_ms: nil)
|
|
145
|
+
@store_base_url = store_base_url.nil? ? nil : Validation.store_base_url!(store_base_url)
|
|
146
|
+
@connect_timeout_ms = Validation.timeout!(connect_timeout_ms, "connect_timeout_ms")
|
|
147
|
+
@timeout_ms = Validation.timeout!(timeout_ms, "timeout_ms")
|
|
148
|
+
@pid = Process.pid
|
|
149
|
+
@rebuild_lock = Mutex.new
|
|
150
|
+
@native = build_native
|
|
38
151
|
end
|
|
39
152
|
|
|
40
|
-
# Evaluate
|
|
153
|
+
# Evaluate every governance policy that applies to an event trigger.
|
|
154
|
+
# +context+ must carry the evaluation scope at `organization.id` as a
|
|
155
|
+
# resource path (e.g. `"organizations/1"`).
|
|
156
|
+
#
|
|
157
|
+
# @param trigger [String] the event, e.g. "deployment_requested".
|
|
158
|
+
# @param context [Hash] the Rego `input` document for every policy in
|
|
159
|
+
# the batch (max 1 MiB as JSON).
|
|
160
|
+
# @param principal [String] who the decision is about, as an opaque
|
|
161
|
+
# path-style identifier string (e.g. `"users/42"`); the caller owns
|
|
162
|
+
# the naming scheme. Trimmed of surrounding whitespace; must not be
|
|
163
|
+
# blank, contain a newline, or exceed 64 KiB once trimmed.
|
|
164
|
+
# @param resource [String] what the decision is about, as an opaque
|
|
165
|
+
# path-style identifier string (e.g.
|
|
166
|
+
# `"organizations/1/deployments/9"`). Same validation as
|
|
167
|
+
# +principal+.
|
|
168
|
+
# @param data [Hash, nil] optional Rego `data` document shared by the
|
|
169
|
+
# batch. It must not define a policy's own decision rules
|
|
170
|
+
# (`violation`/`deny`/`allow`); the engine reports that on the
|
|
171
|
+
# policy's +error+.
|
|
172
|
+
# @param store_authorization [String, nil] the `Authorization` header
|
|
173
|
+
# value the lookup presents to the Policy Store, verbatim (target
|
|
174
|
+
# scheme `"Bearer <jwt>"`). nil sends no header. Never logged or
|
|
175
|
+
# echoed in errors.
|
|
176
|
+
# @raise [ArgumentError] for a caller fault: wrong argument types, a
|
|
177
|
+
# document that does not serialize to JSON, a blank trigger, a blank
|
|
178
|
+
# +principal+ or +resource+ (or one exceeding 64 KiB or carrying a
|
|
179
|
+
# newline), missing organization scope, an oversized input document,
|
|
180
|
+
# or an engine constructed without a store.
|
|
181
|
+
# @raise [RuntimeError] for a backend fault: the store lookup failed
|
|
182
|
+
# or was rejected, or resolved more policies than the per-trigger
|
|
183
|
+
# cap. Never an empty batch, which would read as "nothing to
|
|
184
|
+
# enforce".
|
|
185
|
+
# @return [Hash] `{ decisions: Array<Hash>, undecided_policy_ids:
|
|
186
|
+
# Array<Integer>, identifier: String }`. +decisions+ has one entry
|
|
187
|
+
# per resolved, in-scope policy, in lookup order; empty means
|
|
188
|
+
# nothing applies. Each decision is `{ policy_id: Integer, matched:
|
|
189
|
+
# Boolean, actions: Array<Hash>, reasons: Array<Hash>, error: String
|
|
190
|
+
# | nil }`; each action `{ action_type: String, message: String |
|
|
191
|
+
# nil, params: Hash, gating: Boolean }`; each reason `{ message:
|
|
192
|
+
# String | nil, details: Hash }`. +actions+ are exactly those the
|
|
193
|
+
# store configured on the policy, emitted when it matched. A non-nil
|
|
194
|
+
# +error+ means the policy could not be decided (broken, or over the
|
|
195
|
+
# per-evaluation time budget); +matched+ is then false and its id is
|
|
196
|
+
# listed in +undecided_policy_ids+. +identifier+ is the stable
|
|
197
|
+
# decision identifier derived from (+principal+, +trigger+,
|
|
198
|
+
# +resource+): the same subject always yields the same identifier,
|
|
199
|
+
# so callers can key journaling, action deduplication, and approval
|
|
200
|
+
# correlation on it.
|
|
41
201
|
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# evaluation time budget was exceeded - not the caller's fault.
|
|
53
|
-
# Callers can rely on a successful return meaning the policy
|
|
54
|
-
# evaluated cleanly; there is no in-band error to check.
|
|
55
|
-
# @return [Hash] `{ matched: Boolean, actions: Array<Hash>, reasons:
|
|
56
|
-
# Array<Hash> }`. `matched` is `true` exactly when `actions`
|
|
57
|
-
# (equivalently `reasons`) is non-empty - a convenience so callers
|
|
58
|
-
# don't have to infer "did the policy fire" from array emptiness.
|
|
59
|
-
# It is not itself an allow/deny decision: today every action is
|
|
60
|
-
# `"block"`, so `matched` and "should deny" coincide, but a future
|
|
61
|
-
# Policy Store action type (e.g. `"log"`) could match without
|
|
62
|
-
# implying denial - check `actions` for enforcement decisions. Each
|
|
63
|
-
# action is `{ action_type: String, message: String | nil, params:
|
|
64
|
-
# Hash }`; each reason is `{ message: String | nil, details: Hash
|
|
65
|
-
# }`. Empty +actions+ means the policy allows the operation.
|
|
66
|
-
def evaluate(policy_rego:, context:, data: {})
|
|
202
|
+
# Verdict: deny when any action is gating or any policy is
|
|
203
|
+
# undecided, allow otherwise. +matched+ alone decides nothing.
|
|
204
|
+
def evaluate(trigger:, context:, principal:, resource:, data: nil, store_authorization: nil)
|
|
205
|
+
if @store_base_url.nil?
|
|
206
|
+
raise ArgumentError, "this engine has no Policy Store to resolve policies from: pass store_base_url: to " \
|
|
207
|
+
"Gitlab::Glaz::GovernPolicyEngine.new, or for the shared engine assign " \
|
|
208
|
+
"Gitlab::Glaz::GovernPolicyEngine.policy_store_url or set GLAZ_POLICY_STORE_URL " \
|
|
209
|
+
"(e.g. \"https://gitlab.com/api/v4\")"
|
|
210
|
+
end
|
|
211
|
+
|
|
67
212
|
encoded = ::Glaz::Govern::V1::EvaluateGovernPolicyRequest.encode(
|
|
68
213
|
::Glaz::Govern::V1::EvaluateGovernPolicyRequest.new(
|
|
69
|
-
|
|
70
|
-
input_json: context
|
|
71
|
-
data_json: data.nil?
|
|
214
|
+
trigger: Validation.string!(trigger, "trigger"),
|
|
215
|
+
input_json: Validation.json!(context, "context"),
|
|
216
|
+
data_json: data.nil? ? "" : Validation.json!(data, "data"),
|
|
217
|
+
principal: Validation.string!(principal, "principal"),
|
|
218
|
+
resource: Validation.string!(resource, "resource"),
|
|
219
|
+
store_authorization: Validation.store_authorization!(store_authorization)
|
|
72
220
|
)
|
|
73
221
|
)
|
|
74
222
|
|
|
75
223
|
response = ::Glaz::Govern::V1::EvaluateGovernPolicyResponse.decode(
|
|
76
|
-
|
|
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
|
+
}
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Parse and compile a Rego policy without evaluating it. A policy that
|
|
235
|
+
# fails to parse is not an error: it comes back as
|
|
236
|
+
# `{ valid: false, errors: [...] }` so the Policy Store can surface
|
|
237
|
+
# actionable feedback.
|
|
238
|
+
#
|
|
239
|
+
# @param policy_rego [String] Rego source (max 64 KiB).
|
|
240
|
+
# @raise [ArgumentError] when +policy_rego+ is not a String.
|
|
241
|
+
# @return [Hash] `{ valid: Boolean, errors: Array<Hash> }`; each error
|
|
242
|
+
# is `{ message: String, location: String }` with +location+ in
|
|
243
|
+
# `"LINE:COL"` format, or `""` when none is available.
|
|
244
|
+
def validate(policy_rego:)
|
|
245
|
+
encoded = ::Glaz::Govern::V1::ValidateGovernPolicyRequest.encode(
|
|
246
|
+
::Glaz::Govern::V1::ValidateGovernPolicyRequest.new(
|
|
247
|
+
policy_rego: Validation.string!(policy_rego, "policy_rego")
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
response = ::Glaz::Govern::V1::ValidateGovernPolicyResponse.decode(
|
|
252
|
+
native.validate(encoded)
|
|
77
253
|
)
|
|
78
254
|
|
|
79
255
|
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
actions: response.actions.map { |action| action_hash(action) }
|
|
256
|
+
valid: response.valid,
|
|
257
|
+
errors: response.errors.map { |e| { message: e.message, location: e.location } }
|
|
83
258
|
}
|
|
84
259
|
end
|
|
85
260
|
|
|
86
261
|
# Evaluate a Rego query directly, with no `violation`/`deny`/`allow`
|
|
87
|
-
# interpretation. A debugging
|
|
88
|
-
# policy
|
|
89
|
-
#
|
|
90
|
-
# in-band, since this method is meant for inspecting raw results
|
|
91
|
-
# (including failures) while developing a policy, not for production
|
|
92
|
-
# decision-making.
|
|
262
|
+
# interpretation and no policy lookup. A debugging escape hatch for
|
|
263
|
+
# policy development: evaluation failures stay in-band on +error+
|
|
264
|
+
# rather than raising.
|
|
93
265
|
#
|
|
94
|
-
# @param policy_rego [String] Rego
|
|
95
|
-
# @param context [Hash] the
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
# @
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
|
|
266
|
+
# @param policy_rego [String] Rego source (max 64 KiB).
|
|
267
|
+
# @param context [Hash] the Rego `input` document (max 1 MiB as JSON).
|
|
268
|
+
# @param query [String] e.g. "data.mypackage.violation"; must not be
|
|
269
|
+
# blank.
|
|
270
|
+
# @param data [Hash, nil] optional Rego `data` document, merged as
|
|
271
|
+
# given.
|
|
272
|
+
# @raise [ArgumentError] for wrong argument types or a document that
|
|
273
|
+
# does not serialize to JSON.
|
|
274
|
+
# @return [Hash] `{ result: Object, error: String | nil }`. +result+ is
|
|
275
|
+
# whatever the query evaluated to (parsed JSON, String keys); nil
|
|
276
|
+
# when +error+ is set.
|
|
277
|
+
def debug_evaluate(policy_rego:, context:, query:, data: nil)
|
|
105
278
|
encoded = ::Glaz::Govern::V1::EvaluateGovernPolicyDebugRequest.encode(
|
|
106
279
|
::Glaz::Govern::V1::EvaluateGovernPolicyDebugRequest.new(
|
|
107
|
-
policy_rego: policy_rego,
|
|
108
|
-
query: query
|
|
109
|
-
input_json: context
|
|
110
|
-
data_json: data.nil?
|
|
280
|
+
policy_rego: Validation.string!(policy_rego, "policy_rego"),
|
|
281
|
+
query: Validation.string!(query, "query"),
|
|
282
|
+
input_json: Validation.json!(context, "context"),
|
|
283
|
+
data_json: data.nil? ? "" : Validation.json!(data, "data")
|
|
111
284
|
)
|
|
112
285
|
)
|
|
113
286
|
|
|
114
|
-
response = JSON.parse(
|
|
287
|
+
response = JSON.parse(native.debug_evaluate(encoded), max_nesting: false)
|
|
115
288
|
|
|
116
289
|
{
|
|
117
|
-
result: response[
|
|
118
|
-
error:
|
|
290
|
+
result: response["result"],
|
|
291
|
+
error: Validation.presence(response["error"])
|
|
119
292
|
}
|
|
120
293
|
end
|
|
121
294
|
|
|
122
295
|
private
|
|
123
296
|
|
|
297
|
+
# A forked child inherits the parent's engine and its pooled HTTP
|
|
298
|
+
# sockets, so the first call after a fork rebuilds it. The lock is only
|
|
299
|
+
# taken on that path, so concurrent first calls in the child (a Puma
|
|
300
|
+
# worker's request threads) share one rebuild. Ruby releases a mutex
|
|
301
|
+
# whose owner did not survive the fork, so the child cannot inherit it
|
|
302
|
+
# locked.
|
|
303
|
+
def native
|
|
304
|
+
return @native if @pid == Process.pid
|
|
305
|
+
|
|
306
|
+
@rebuild_lock.synchronize do
|
|
307
|
+
if @pid != Process.pid
|
|
308
|
+
@native = build_native
|
|
309
|
+
@pid = Process.pid
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
@native
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def build_native
|
|
316
|
+
::Glaz::Native::GovernPolicyEngine.new(@store_base_url, @connect_timeout_ms, @timeout_ms)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def decision_hash(decision)
|
|
320
|
+
{
|
|
321
|
+
policy_id: decision.policy_id,
|
|
322
|
+
matched: decision.matched,
|
|
323
|
+
actions: decision.actions.map { |action| action_hash(action) },
|
|
324
|
+
reasons: decision.reasons.map { |reason| reason_hash(reason) },
|
|
325
|
+
error: Validation.presence(decision.error)
|
|
326
|
+
}
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# The native layer already bounds nesting, so parse without a limit.
|
|
124
330
|
def action_hash(action)
|
|
125
331
|
{
|
|
126
332
|
action_type: action.action_type,
|
|
127
|
-
message:
|
|
128
|
-
params: JSON.parse(action.params_json)
|
|
333
|
+
message: Validation.presence(action.message),
|
|
334
|
+
params: JSON.parse(action.params_json, max_nesting: false),
|
|
335
|
+
gating: action.gating
|
|
129
336
|
}
|
|
130
337
|
end
|
|
131
338
|
|
|
132
339
|
def reason_hash(reason)
|
|
133
340
|
{
|
|
134
|
-
message:
|
|
135
|
-
details: JSON.parse(reason.details_json)
|
|
341
|
+
message: Validation.presence(reason.message),
|
|
342
|
+
details: JSON.parse(reason.details_json, max_nesting: false)
|
|
136
343
|
}
|
|
137
344
|
end
|
|
138
345
|
end
|
data/lib/gitlab/glaz/version.rb
CHANGED
data/lib/gitlab/glaz.rb
CHANGED
|
@@ -24,12 +24,12 @@ module Gitlab
|
|
|
24
24
|
end.freeze
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
# Returns the
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# the
|
|
27
|
+
# Returns GovernPolicyEngine.shared, the engine configured through
|
|
28
|
+
# GovernPolicyEngine.policy_store_url= / GLAZ_POLICY_STORE_URL. It is
|
|
29
|
+
# rebuilt when that configuration changes, so call this each time rather
|
|
30
|
+
# than caching the result.
|
|
31
31
|
def govern_policy_engine
|
|
32
|
-
|
|
32
|
+
GovernPolicyEngine.shared
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
end
|
data/lib/proto/govern_pb.rb
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
require 'google/protobuf'
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
descriptor_data = "\n\x12proto/govern.proto\x12\x0eglaz.govern.v1\"
|
|
8
|
+
descriptor_data = "\n\x12proto/govern.proto\x12\x0eglaz.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\"2\n\x1bValidateGovernPolicyRequest\x12\x13\n\x0bpolicy_rego\x18\x01 \x01(\t\":\n\x15GovernValidationError\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x10\n\x08location\x18\x02 \x01(\t\"d\n\x1cValidateGovernPolicyResponse\x12\r\n\x05valid\x18\x01 \x01(\x08\x12\x35\n\x06\x65rrors\x18\x02 \x03(\x0b\x32%.glaz.govern.v1.GovernValidationError\"\x97\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\"]\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\"\x97\x01\n\x13PolicyApplicability\x12\x11\n\tpolicy_id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x34\n\rapplicability\x18\x03 \x01(\x0e\x32\x1d.glaz.govern.v1.Applicability\x12\x1a\n\x12missing_dimensions\x18\x04 \x03(\t\x12\r\n\x05\x65rror\x18\x05 \x01(\t\"S\n\x1aListGovernPoliciesResponse\x12\x35\n\x08policies\x18\x01 \x03(\x0b\x32#.glaz.govern.v1.PolicyApplicability\"\xa1\x01\n\x0ePolicyDecision\x12\x11\n\tpolicy_id\x18\x01 \x01(\x04\x12\x0f\n\x07matched\x18\x02 \x01(\x08\x12-\n\x07\x61\x63tions\x18\x03 \x03(\x0b\x32\x1c.glaz.govern.v1.GovernAction\x12-\n\x07reasons\x18\x04 \x03(\x0b\x32\x1c.glaz.govern.v1.GovernReason\x12\r\n\x05\x65rror\x18\x05 \x01(\t\"Q\n\x10GovernDiagnostic\x12,\n\x04kind\x18\x01 \x01(\x0e\x32\x1e.glaz.govern.v1.DiagnosticKind\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xba\x01\n\x1c\x45valuateGovernPolicyResponse\x12\x31\n\tdecisions\x18\x01 \x03(\x0b\x32\x1e.glaz.govern.v1.PolicyDecision\x12\x1c\n\x14undecided_policy_ids\x18\x02 \x03(\x04\x12\x35\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32 .glaz.govern.v1.GovernDiagnostic\x12\x12\n\nidentifier\x18\x04 \x01(\t*\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\x42?Z=gitlab.com/gitlab-org/govern/glaz-client-go/governv1;governv1b\x06proto3"
|
|
9
9
|
|
|
10
10
|
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
11
11
|
pool.add_serialized_file(descriptor_data)
|
|
@@ -13,11 +13,21 @@ pool.add_serialized_file(descriptor_data)
|
|
|
13
13
|
module Glaz
|
|
14
14
|
module Govern
|
|
15
15
|
module V1
|
|
16
|
-
EvaluateGovernPolicyRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.EvaluateGovernPolicyRequest").msgclass
|
|
17
16
|
EvaluateGovernPolicyDebugRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.EvaluateGovernPolicyDebugRequest").msgclass
|
|
18
17
|
GovernAction = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.GovernAction").msgclass
|
|
19
18
|
GovernReason = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.GovernReason").msgclass
|
|
19
|
+
ValidateGovernPolicyRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.ValidateGovernPolicyRequest").msgclass
|
|
20
|
+
GovernValidationError = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.GovernValidationError").msgclass
|
|
21
|
+
ValidateGovernPolicyResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.ValidateGovernPolicyResponse").msgclass
|
|
22
|
+
EvaluateGovernPolicyRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.EvaluateGovernPolicyRequest").msgclass
|
|
23
|
+
ListGovernPoliciesRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.ListGovernPoliciesRequest").msgclass
|
|
24
|
+
PolicyApplicability = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.PolicyApplicability").msgclass
|
|
25
|
+
ListGovernPoliciesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.ListGovernPoliciesResponse").msgclass
|
|
26
|
+
PolicyDecision = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.PolicyDecision").msgclass
|
|
27
|
+
GovernDiagnostic = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.GovernDiagnostic").msgclass
|
|
20
28
|
EvaluateGovernPolicyResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.EvaluateGovernPolicyResponse").msgclass
|
|
29
|
+
Applicability = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.Applicability").enummodule
|
|
30
|
+
DiagnosticKind = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.DiagnosticKind").enummodule
|
|
21
31
|
end
|
|
22
32
|
end
|
|
23
33
|
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:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- group::authorization
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: google-protobuf
|
|
@@ -116,14 +116,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
116
116
|
requirements:
|
|
117
117
|
- - ">="
|
|
118
118
|
- !ruby/object:Gem::Version
|
|
119
|
-
version: '3.
|
|
119
|
+
version: '3.3'
|
|
120
120
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
121
|
requirements:
|
|
122
122
|
- - ">="
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
124
|
version: '0'
|
|
125
125
|
requirements: []
|
|
126
|
-
rubygems_version: 3.
|
|
126
|
+
rubygems_version: 3.5.22
|
|
127
127
|
signing_key:
|
|
128
128
|
specification_version: 4
|
|
129
129
|
summary: Ruby client for the GLAZ authorization engine
|