gitlab-glaz 2.0.0-aarch64-linux-gnu → 2.2.0-aarch64-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.
data/Cargo.toml CHANGED
@@ -4,14 +4,14 @@ members = ["ext/*"]
4
4
  resolver = "2"
5
5
 
6
6
  [workspace.package]
7
- version = "2.0.0"
7
+ version = "2.2.0"
8
8
  edition = "2024"
9
9
 
10
10
  # Shared dependencies for all crates
11
11
  [workspace.dependencies]
12
- magnus = { version = "0.8.2", features = ["rb-sys"] }
12
+ magnus = { version = "0.9.0", features = ["rb-sys"] }
13
13
  rb-sys = "0.9.128"
14
14
  prost = "0.14"
15
- glaz-module = { git = "https://gitlab.com/gitlab-org/auth/glaz.git", rev = "0c90ffcd6e6f74b7a1998e196e7d324ad7d54b56" }
16
- glaz-roles = { git = "https://gitlab.com/gitlab-org/auth/glaz.git", rev = "0c90ffcd6e6f74b7a1998e196e7d324ad7d54b56" }
17
- glaz-proto = { git = "https://gitlab.com/gitlab-org/auth/glaz.git", rev = "0c90ffcd6e6f74b7a1998e196e7d324ad7d54b56" }
15
+ glaz-module = { git = "https://gitlab.com/gitlab-org/auth/glaz.git", rev = "432611dc73b9a98cf0a2557225155c768b9a14ea" }
16
+ glaz-roles = { git = "https://gitlab.com/gitlab-org/auth/glaz.git", rev = "432611dc73b9a98cf0a2557225155c768b9a14ea" }
17
+ glaz-proto = { git = "https://gitlab.com/gitlab-org/auth/glaz.git", rev = "432611dc73b9a98cf0a2557225155c768b9a14ea" }
data/README.md CHANGED
@@ -39,7 +39,24 @@ Schemas must declare `User` and `Resource` entities (checks build `User::"<subje
39
39
 
40
40
  ### Governance policies
41
41
 
42
- `Gitlab::Glaz::GovernPolicyEngine` evaluates the Rego governance policies that apply to an event trigger. The engine fetches them itself from the Policy Store REST API, so name the store first:
42
+ `Gitlab::Glaz::GovernPolicyEngine` evaluates the Rego governance policies that apply to an event trigger. A caller that already has the policies (the Policy Store itself, running the engine in-process) passes the active policies of the trigger and organization on the call, shaped like the store's policy entity, and no store is contacted:
43
+
44
+ ```ruby
45
+ result = Gitlab::Glaz.govern_policy_engine.evaluate(
46
+ trigger: 'deployment_requested',
47
+ context: { organization: { id: 'organizations/1' }, environment: { tier: 'production' }, approvals: [] },
48
+ principal: 'users/42',
49
+ resource: 'organizations/1/deployments/9',
50
+ policies: [
51
+ { id: 1, name: 'Approve production', trigger_type: 'deployment_requested', organization_id: 1,
52
+ namespace_id: nil, # the owning group's id, or nil for an organization-level policy
53
+ mode: 'enforce', scope_rego: nil, scope_dimensions: [], policy_rego: "package governance\n...",
54
+ actions: [{ type: 'block', value: { message: 'Needs approval' } }] }
55
+ ]
56
+ )
57
+ ```
58
+
59
+ A policy that belongs to another trigger or organization, or to a namespace outside the hierarchy the context names in `groups`, raises `ArgumentError` for the whole call. An empty Array is a successful, empty evaluation. Any other caller leaves `policies:` out and the engine fetches the policies from the Policy Store REST API, so name the store first:
43
60
 
44
61
  ```ruby
45
62
  Gitlab::Glaz::GovernPolicyEngine.policy_store_url = 'https://gitlab.com/api/v4'
@@ -52,14 +69,20 @@ result = Gitlab::Glaz.govern_policy_engine.evaluate(
52
69
  resource: 'organizations/1/deployments/9', # what the decision is about
53
70
  store_authorization: "Bearer #{token}" # per request; nil sends no Authorization header
54
71
  )
55
- # => { decisions: [{ policy_id: 1, matched: true, actions: [{ action_type: 'block', gating: true, ... }], ... }],
72
+ # => { decisions: [{ policy_id: 1, name: 'Approve production', mode: 'enforce', matched: true,
73
+ # actions: [{ action_type: 'block', gating: true, ... }], ... }],
56
74
  # undecided_policy_ids: [],
57
- # identifier: 'ed9cb624-7d8f-51eb-ad5a-ef394efcee74' }
75
+ # identifier: 'ed9cb624-7d8f-51eb-ad5a-ef394efcee74',
76
+ # dry_run: false }
58
77
  ```
59
78
 
60
- Each call makes one `GET {store_url}/organizations/{id}/security/policy_store?trigger_type={trigger}` (500 ms connect / 2 s total timeout by default, no redirects, TLS against the platform trust store, `*_PROXY`/`NO_PROXY` honoured), so egress rules must allow it. Deny when any returned action is `gating` or `undecided_policy_ids` is non-empty; allow otherwise. Caller mistakes raise `ArgumentError`, store faults raise `RuntimeError`, and a policy that could not be decided (broken, or over the per-evaluation time budget) is reported in-band on its decision's `error` and listed in `undecided_policy_ids`. `data` must not define a policy's own `violation`/`deny`/`allow` rules. `principal` and `resource` name the decision subject (each trimmed, and rejected if blank, newline-carrying, or over 64 KiB); the response's `identifier` is a stable UUIDv5 over `(principal, trigger, resource)`, so re-evaluating the same subject yields the same identifier for journaling, action deduplication, and approval correlation.
79
+ Each call without `policies:` makes one `GET {store_url}/organizations/{id}/security/policy_store?trigger_type={trigger}` (500 ms connect / 2 s total timeout by default, no redirects, TLS against the platform trust store, `*_PROXY`/`NO_PROXY` honoured), so egress rules must allow it. Deny when any returned action is `gating` or `undecided_policy_ids` is non-empty; allow otherwise. Caller mistakes raise `ArgumentError`, store faults raise `RuntimeError`, and a policy that could not be decided (broken, or over the per-evaluation time budget) is reported in-band on its decision's `error` and listed in `undecided_policy_ids`. `data` must not define a policy's own `violation`/`deny`/`allow` rules. `principal` and `resource` name the decision subject (each trimmed, and rejected if blank, newline-carrying, or over 64 KiB); the response's `identifier` is a stable UUIDv5 over `(principal, trigger, resource)`, so re-evaluating the same subject yields the same identifier for journaling, action deduplication, and approval correlation.
80
+
81
+ `#evaluate_dry_run` takes the same arguments and returns the same result with `dry_run: true`, to preview what the policies would decide. It is never a gate: do not deny on its result, and nothing records it.
82
+
83
+ `#list_policies(trigger:, context:, store_authorization: nil, policies: nil)` answers a different question: which policies could ever apply here. It needs only the trigger and the lookup keys (`organization.id`, plus, for the project settings triggers, the project's namespace hierarchy: `groups`, and `project.namespace_id` for a personal project), runs no policy's rules and returns `{ policies: [{ policy_id:, name:, applicability:, missing_dimensions:, error: }] }`, with nothing to gate on. `applicability` is `"applies"`, `"not_applicable"` or `"might_apply"`; a `"might_apply"` row lists in `missing_dimensions` the context paths to supply to settle it. A row whose scope could not be evaluated has a nil `applicability` and its `error`.
61
84
 
62
- Without a store URL the shared engine still serves `#validate` (save-time parse check) and `#debug_evaluate` (raw single-policy evaluation); only trigger-keyed evaluation needs the store. See the class documentation for the full contract.
85
+ Without a store URL the shared engine still serves `#validate` (save-time parse check) and `#debug_evaluate` (raw single-policy evaluation); only `#evaluate`, `#evaluate_dry_run` and `#list_policies` need the store, and only when called without `policies:`. See the class documentation for the full contract.
63
86
 
64
87
  ## Development
65
88
 
data/ext/glaz/src/lib.rs CHANGED
@@ -59,8 +59,10 @@ impl PermissionCheckEngine {
59
59
  }
60
60
 
61
61
  /// Thin 1:1 wrapper around `glaz_module::GovernPolicyCheckEngine`, the Rego
62
- /// governance evaluator. The policy lookup fetches from the Policy Store
63
- /// REST API named at construction; authentication is per request, on
62
+ /// governance evaluator. A request may carry its own policies
63
+ /// (`EvaluateGovernPolicyRequest.policies`); then no lookup runs. Otherwise
64
+ /// the lookup fetches from the Policy Store REST API named at construction,
65
+ /// authenticated per request on
64
66
  /// `EvaluateGovernPolicyRequest.store_authorization`. Stateless between
65
67
  /// calls. `evaluate_policies` raises on a whole-call failure (matching
66
68
  /// `PermissionCheckEngine`'s convention): `ArgumentError` for caller
@@ -81,8 +83,9 @@ impl GovernPolicyEngine {
81
83
  /// `base_url` is the Policy Store REST API root (e.g.
82
84
  /// `"https://gitlab.com/api/v4"`); `nil` timeouts keep
83
85
  /// `glaz-govern`'s defaults. Without `base_url` the engine has no policy
84
- /// source: `validate` and `debug_evaluate` work, `evaluate_policies`
85
- /// fails closed (see `NoStore`).
86
+ /// lookup: `validate` and `debug_evaluate` work, and `evaluate_policies`
87
+ /// works for a request that carries its own policies. Any other
88
+ /// `evaluate_policies` call fails closed (see `NoStore`).
86
89
  fn initialize(
87
90
  base_url: Option<String>,
88
91
  connect_timeout_ms: Option<u64>,
@@ -130,10 +133,10 @@ impl GovernPolicyEngine {
130
133
  }
131
134
 
132
135
  /// Trigger-keyed batch evaluation: takes an encoded
133
- /// `EvaluateGovernPolicyRequest`, resolves the applicable policies from
134
- /// the Policy Store (keyed on `trigger` and the context's
135
- /// `organization.id`) and returns the encoded
136
- /// `EvaluateGovernPolicyResponse`. Raises `ArgumentError` for caller
136
+ /// `EvaluateGovernPolicyRequest`, takes the policies from the request's
137
+ /// `policies` when set and from the Policy Store otherwise (keyed on
138
+ /// `trigger` and the context's `organization.id`), and returns the
139
+ /// encoded `EvaluateGovernPolicyResponse`. Raises `ArgumentError` for caller
137
140
  /// faults and `RuntimeError` for backend faults — never an empty batch,
138
141
  /// which would read fail-open. A broken individual policy is reported
139
142
  /// in-band on its decision's `error`.
@@ -161,6 +164,56 @@ impl GovernPolicyEngine {
161
164
  Ok(ruby.str_from_slice(&response))
162
165
  }
163
166
 
167
+ /// Dry-run twin of `evaluate_policies` (GOVERN-004: a method of its own,
168
+ /// never a flag). Takes an encoded `EvaluateGovernPolicyRequest` with
169
+ /// `dry_run` set and returns the encoded response echoing it; same
170
+ /// errors, GVL release and thread handling as `evaluate_policies`.
171
+ fn evaluate_policies_dry_run(
172
+ ruby: &Ruby,
173
+ rb_self: &Self,
174
+ proto_bytes: magnus::RString,
175
+ ) -> Result<magnus::RString, Error> {
176
+ // Copy the request out of the Ruby string first: once the GVL is
177
+ // released the GC may move or free the buffer behind `as_slice`.
178
+ let request = unsafe { proto_bytes.as_slice() }.to_vec();
179
+ let engine = Arc::clone(&rb_self.engine);
180
+ let response = without_gvl(ruby, move || {
181
+ on_own_thread(move || {
182
+ engine
183
+ .evaluate_policies_dry_run(&request)
184
+ .map(|response| response.encode_to_vec())
185
+ })
186
+ })?
187
+ .map_err(|e| map_error(ruby, e))?;
188
+ Ok(ruby.str_from_slice(&response))
189
+ }
190
+
191
+ /// List an encoded `ListGovernPoliciesRequest`: every policy the lookup
192
+ /// resolves for the trigger and scope, each with whether it applies to
193
+ /// the context supplied. Evaluates no policy's rules, so the encoded
194
+ /// `ListGovernPoliciesResponse` carries nothing to gate on. Same errors,
195
+ /// GVL release and thread handling as `evaluate_policies`; a policy whose
196
+ /// scope fails to evaluate comes back in-band on its own row.
197
+ fn list_policies(
198
+ ruby: &Ruby,
199
+ rb_self: &Self,
200
+ proto_bytes: magnus::RString,
201
+ ) -> Result<magnus::RString, Error> {
202
+ // Copy the request out of the Ruby string first: once the GVL is
203
+ // released the GC may move or free the buffer behind `as_slice`.
204
+ let request = unsafe { proto_bytes.as_slice() }.to_vec();
205
+ let engine = Arc::clone(&rb_self.engine);
206
+ let response = without_gvl(ruby, move || {
207
+ on_own_thread(move || {
208
+ engine
209
+ .list_policies(&request)
210
+ .map(|response| response.encode_to_vec())
211
+ })
212
+ })?
213
+ .map_err(|e| map_error(ruby, e))?;
214
+ Ok(ruby.str_from_slice(&response))
215
+ }
216
+
164
217
  /// Parse and compile a Rego policy without evaluating it against input.
165
218
  ///
166
219
  /// Infallible for policy errors: parse failures and oversized input are
@@ -195,8 +248,9 @@ impl GovernPolicyEngine {
195
248
 
196
249
  /// The lookup of an engine constructed without a Policy Store: every
197
250
  /// lookup fails closed as `LookupError::Unavailable` (a `RuntimeError`), so
198
- /// such an engine can never report "nothing to enforce". The Ruby wrapper
199
- /// rejects the call earlier with configuration guidance.
251
+ /// such an engine can never report "nothing to enforce" for a request
252
+ /// without its own policies. The Ruby wrapper rejects that call earlier
253
+ /// with configuration guidance.
200
254
  struct NoStore;
201
255
 
202
256
  impl glaz_module::PolicyLookup for NoStore {
@@ -213,12 +267,18 @@ impl glaz_module::PolicyLookup for NoStore {
213
267
 
214
268
  /// Return the default roles embedded in the extension at build time (parsed
215
269
  /// from glaz-roles' roles/**/*.yml) as an array of hashes shaped like
216
- /// `{ id: String, name: String, permissions: [String, ...] }`.
270
+ /// `{ id: String, key: String, name: String, permissions: [String, ...] }`.
271
+ ///
272
+ /// `key` is the role's stable machine identifier (e.g. `artifact_admin`),
273
+ /// distinct from the human-facing `name` and from the `id` callers put in
274
+ /// relationships. Callers reference a role by `key` instead of hardcoding
275
+ /// its UUID or matching on its display name.
217
276
  fn roles(ruby: &Ruby) -> Result<magnus::RArray, Error> {
218
277
  let array = ruby.ary_new();
219
278
  for role in glaz_roles::default_roles() {
220
279
  let hash = ruby.hash_new();
221
280
  hash.aset(ruby.sym_new("id"), role.id.to_string())?;
281
+ hash.aset(ruby.sym_new("key"), role.key)?;
222
282
  hash.aset(ruby.sym_new("name"), role.name)?;
223
283
  let permissions = ruby.ary_new();
224
284
  for permission in &role.permissions {
@@ -374,6 +434,14 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
374
434
  "evaluate_policies",
375
435
  magnus::method!(GovernPolicyEngine::evaluate_policies, 1),
376
436
  )?;
437
+ govern.define_method(
438
+ "evaluate_policies_dry_run",
439
+ magnus::method!(GovernPolicyEngine::evaluate_policies_dry_run, 1),
440
+ )?;
441
+ govern.define_method(
442
+ "list_policies",
443
+ magnus::method!(GovernPolicyEngine::list_policies, 1),
444
+ )?;
377
445
  govern.define_method("validate", magnus::method!(GovernPolicyEngine::validate, 1))?;
378
446
  Ok(())
379
447
  }
Binary file
Binary file
Binary file
Binary file
Binary file