gitlab-glaz 1.2.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 +678 -30
- data/Cargo.toml +4 -4
- data/README.md +25 -1
- data/ext/glaz/Cargo.toml +1 -0
- data/ext/glaz/src/lib.rs +227 -49
- data/lib/gitlab/glaz/govern_policy_engine.rb +279 -105
- data/lib/gitlab/glaz/version.rb +1 -1
- data/lib/gitlab/glaz.rb +5 -5
- data/lib/proto/govern_pb.rb +10 -3
- metadata +3 -3
|
@@ -1,113 +1,255 @@
|
|
|
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
|
|
38
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
|
|
39
115
|
|
|
40
|
-
|
|
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
|
|
151
|
+
end
|
|
152
|
+
|
|
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"`).
|
|
41
156
|
#
|
|
42
|
-
# @param
|
|
43
|
-
# @param context [Hash] the
|
|
44
|
-
#
|
|
45
|
-
# @param
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
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.
|
|
201
|
+
#
|
|
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)
|
|
77
225
|
)
|
|
78
226
|
|
|
79
227
|
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
228
|
+
decisions: response.decisions.map { |decision| decision_hash(decision) },
|
|
229
|
+
undecided_policy_ids: response.undecided_policy_ids.to_a,
|
|
230
|
+
identifier: response.identifier
|
|
83
231
|
}
|
|
84
232
|
end
|
|
85
233
|
|
|
86
|
-
#
|
|
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.
|
|
87
238
|
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
# @param policy_rego [String] Rego policy source text (UTF-8, max 64 KiB).
|
|
94
|
-
# Must be a valid UTF-8 string; raises `Encoding::InvalidByteSequenceError`
|
|
95
|
-
# if the string contains invalid byte sequences.
|
|
96
|
-
# @return [Hash] `{ valid: Boolean, errors: Array<Hash> }`.
|
|
97
|
-
# `valid` is `true` when the policy parsed and compiled successfully.
|
|
98
|
-
# `errors` is empty when valid; each entry is
|
|
99
|
-
# `{ message: String, location: String }` where `location` is a
|
|
100
|
-
# source-location hint in `"LINE:COL"` format (e.g. `"2:1"`) when the
|
|
101
|
-
# engine surfaces one, or `""` when no separate location is available.
|
|
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.
|
|
102
244
|
def validate(policy_rego:)
|
|
103
245
|
encoded = ::Glaz::Govern::V1::ValidateGovernPolicyRequest.encode(
|
|
104
246
|
::Glaz::Govern::V1::ValidateGovernPolicyRequest.new(
|
|
105
|
-
policy_rego: policy_rego
|
|
247
|
+
policy_rego: Validation.string!(policy_rego, "policy_rego")
|
|
106
248
|
)
|
|
107
249
|
)
|
|
108
250
|
|
|
109
251
|
response = ::Glaz::Govern::V1::ValidateGovernPolicyResponse.decode(
|
|
110
|
-
|
|
252
|
+
native.validate(encoded)
|
|
111
253
|
)
|
|
112
254
|
|
|
113
255
|
{
|
|
@@ -117,55 +259,87 @@ module Gitlab
|
|
|
117
259
|
end
|
|
118
260
|
|
|
119
261
|
# Evaluate a Rego query directly, with no `violation`/`deny`/`allow`
|
|
120
|
-
# interpretation. A debugging
|
|
121
|
-
# policy
|
|
122
|
-
#
|
|
123
|
-
# in-band, since this method is meant for inspecting raw results
|
|
124
|
-
# (including failures) while developing a policy, not for production
|
|
125
|
-
# 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.
|
|
126
265
|
#
|
|
127
|
-
# @param policy_rego [String] Rego
|
|
128
|
-
# @param context [Hash] the
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
# @
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
|
|
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)
|
|
138
278
|
encoded = ::Glaz::Govern::V1::EvaluateGovernPolicyDebugRequest.encode(
|
|
139
279
|
::Glaz::Govern::V1::EvaluateGovernPolicyDebugRequest.new(
|
|
140
|
-
policy_rego: policy_rego,
|
|
141
|
-
query: query
|
|
142
|
-
input_json: context
|
|
143
|
-
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")
|
|
144
284
|
)
|
|
145
285
|
)
|
|
146
286
|
|
|
147
|
-
response = JSON.parse(
|
|
287
|
+
response = JSON.parse(native.debug_evaluate(encoded), max_nesting: false)
|
|
148
288
|
|
|
149
289
|
{
|
|
150
|
-
result: response[
|
|
151
|
-
error:
|
|
290
|
+
result: response["result"],
|
|
291
|
+
error: Validation.presence(response["error"])
|
|
152
292
|
}
|
|
153
293
|
end
|
|
154
294
|
|
|
155
295
|
private
|
|
156
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.
|
|
157
330
|
def action_hash(action)
|
|
158
331
|
{
|
|
159
332
|
action_type: action.action_type,
|
|
160
|
-
message:
|
|
161
|
-
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
|
|
162
336
|
}
|
|
163
337
|
end
|
|
164
338
|
|
|
165
339
|
def reason_hash(reason)
|
|
166
340
|
{
|
|
167
|
-
message:
|
|
168
|
-
details: JSON.parse(reason.details_json)
|
|
341
|
+
message: Validation.presence(reason.message),
|
|
342
|
+
details: JSON.parse(reason.details_json, max_nesting: false)
|
|
169
343
|
}
|
|
170
344
|
end
|
|
171
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,14 +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
|
|
20
|
-
EvaluateGovernPolicyResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.EvaluateGovernPolicyResponse").msgclass
|
|
21
19
|
ValidateGovernPolicyRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.ValidateGovernPolicyRequest").msgclass
|
|
22
20
|
GovernValidationError = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.GovernValidationError").msgclass
|
|
23
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
|
|
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
|
|
24
31
|
end
|
|
25
32
|
end
|
|
26
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-09-
|
|
11
|
+
date: 2026-09-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: google-protobuf
|
|
@@ -123,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
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
|