gitlab-glaz 0.0.3-aarch64-linux-gnu → 1.1.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,11 +4,14 @@ members = ["ext/*"]
4
4
  resolver = "2"
5
5
 
6
6
  [workspace.package]
7
- version = "0.0.3"
7
+ version = "1.1.0"
8
8
  edition = "2024"
9
9
 
10
10
  # Shared dependencies for all crates
11
11
  [workspace.dependencies]
12
12
  magnus = { version = "0.8.2", features = ["rb-sys"] }
13
13
  rb-sys = "0.9.128"
14
- glaz-module = { git = "https://gitlab.com/gitlab-org/auth/glaz.git", rev = "e1100ed8d93b3db58627229b7c4e16e29905a3d0" }
14
+ prost = "0.14"
15
+ glaz-module = { git = "https://gitlab.com/gitlab-org/auth/glaz.git", rev = "af9019a186fbb42cede0ad5ba7496aff2f88cb41" }
16
+ glaz-roles = { git = "https://gitlab.com/gitlab-org/auth/glaz.git", rev = "af9019a186fbb42cede0ad5ba7496aff2f88cb41" }
17
+ glaz-proto = { git = "https://gitlab.com/gitlab-org/auth/glaz.git", rev = "af9019a186fbb42cede0ad5ba7496aff2f88cb41" }
data/README.md CHANGED
@@ -3,68 +3,52 @@
3
3
  > [!WARNING]
4
4
  > This gem is currently designed entirely for internal use at GitLab.
5
5
 
6
- Ruby bindings for the [Glaz](https://gitlab.com/gitlab-org/auth/glaz) authorization engine. It wraps the Rust-backed `glaz-ruby` crate via [Magnus](https://github.com/matsadler/magnus) FFI, exposing the Glaz `CheckEngine` to Ruby without leaving the Ruby process.
7
-
8
- ## Requirements
9
-
10
- - Ruby >= 3.1
11
- - Rust toolchain (for building the native extension)
6
+ Ruby bindings for the [Glaz](https://gitlab.com/gitlab-org/auth/glaz) authorization engine. A thin [Magnus](https://github.com/matsadler/magnus) FFI extension exposes the Rust `glaz-module` engine to Ruby, so Cedar policy checks run in-process.
12
7
 
13
8
  ## Installation
14
9
 
15
- Add to your Gemfile:
10
+ Requires Ruby >= 3.1 and a Rust toolchain. Add to your Gemfile:
16
11
 
17
12
  ```ruby
18
13
  gem 'gitlab-glaz', path: 'gems/gitlab-glaz'
19
14
  ```
20
15
 
21
- ## Building
22
-
23
- The gem contains a Rust extension that must be compiled before use:
24
-
25
- ```shell
26
- bundle exec rake compile
27
- ```
28
-
29
- This compiles the Rust crate in `ext/glaz/` and places the resulting native library under `lib/glaz/`.
30
-
31
16
  ## Usage
32
17
 
33
18
  ```ruby
34
19
  require 'gitlab-glaz'
35
20
 
36
- # Use the Glaz CheckEngine via the compiled native extension
37
- ```
21
+ engine = Gitlab::Glaz::Engine.new(schema: cedar_schema, policies: cedar_policies)
38
22
 
39
- ## Architecture
23
+ engine.check_action(
24
+ subject_uuid: user_uuid,
25
+ object_uuid: project_uuid,
26
+ action: 'read_docs',
27
+ context: {}
28
+ )
29
+ # => { allowed: true, reason: 'permit_granted' }
40
30
 
41
- | Layer | Technology | Purpose |
42
- | ------- | ----------- | --------- |
43
- | Ruby API | `lib/gitlab-glaz.rb` | Entry point, loads the native extension |
44
- | FFI bridge | [Magnus](https://github.com/matsadler/magnus) + [rb-sys](https://github.com/oxidize-rb/rb-sys) | Bridges Ruby ↔ Rust |
45
- | Rust extension | `ext/glaz/` | Thin `#[magnus::init]` shim that delegates to `glaz-ruby` |
46
- | Core engine | [`glaz-ruby`](https://gitlab.com/gitlab-org/auth/glaz) | Rust crate implementing the Glaz authorization engine |
31
+ # Swap the policy set at runtime; the previous set stays active if the
32
+ # new source fails to parse or validate against the schema:
33
+ engine.load_policies(new_cedar_policies)
34
+ ```
47
35
 
48
- ## Development
36
+ The schema is loaded once at construction and cannot change; build a new engine to use a new schema.
49
37
 
50
- The workspace root [`Cargo.toml`](Cargo.toml) declares all shared Rust dependencies. The extension crate lives in [`ext/glaz/`](ext/glaz/).
38
+ Schemas must declare `User` and `Resource` entities (checks build `User::"<subject_uuid>"` and `Resource::"<object_uuid>"` UIDs), and every action's context type must include `permissions: Set<Action>`, which the engine injects with the checked action.
51
39
 
52
- To build only the Rust extension:
40
+ ## Development
53
41
 
54
42
  ```shell
55
- cargo build --release
43
+ bundle exec rake compile # build the native extension (required before rspec)
44
+ bundle exec rspec
56
45
  ```
57
46
 
58
- To run the full compile via Rake (used during gem installation):
59
-
60
- ```shell
61
- bundle exec rake
62
- ```
47
+ The extension crate lives in [`ext/glaz/`](ext/glaz/); shared Rust dependencies are declared in the root [`Cargo.toml`](Cargo.toml).
63
48
 
64
49
  ## Releasing a new version
65
50
 
66
- To release a new version, create a merge request and use the `Release` template, following its instructions.
67
- Once merged, the new version with precompiled, native gems will automatically be published to RubyGems.
51
+ Create a merge request using the `Release` template and follow its instructions. Once merged, precompiled native gems are published to RubyGems automatically.
68
52
 
69
53
  ## License
70
54
 
data/ext/glaz/Cargo.toml CHANGED
@@ -9,4 +9,7 @@ crate-type = ["cdylib"]
9
9
 
10
10
  [dependencies]
11
11
  glaz-module = { workspace = true }
12
+ glaz-proto = { workspace = true }
13
+ glaz-roles = { workspace = true }
12
14
  magnus = { workspace = true }
15
+ prost = { workspace = true }
data/ext/glaz/src/lib.rs CHANGED
@@ -1,26 +1,49 @@
1
1
  use magnus::{Error, Ruby, prelude::*};
2
+ use prost::Message;
2
3
 
3
- #[magnus::wrap(class = "Glaz::Engine::CheckPermission")]
4
- struct CheckPermission {
4
+ /// Thin 1:1 wrapper around `glaz_module::PermissionCheckEngine`. The engine
5
+ /// starts empty; `load_schema` must be called before `load_policies`, and
6
+ /// `check_action` fails with a RuntimeError until both are loaded.
7
+ #[magnus::wrap(class = "Glaz::PermissionCheckEngine")]
8
+ struct PermissionCheckEngine {
5
9
  engine: glaz_module::PermissionCheckEngine,
6
10
  }
7
11
 
8
- impl CheckPermission {
12
+ impl PermissionCheckEngine {
9
13
  fn initialize(ruby: &Ruby) -> Result<Self, Error> {
10
- glaz_module::PermissionCheckEngine::new()
11
- .map(|engine| Self { engine })
12
- .map_err(|e| Error::new(ruby.exception_runtime_error(), format!("{e}")))
14
+ let engine = glaz_module::PermissionCheckEngine::new().map_err(|e| map_error(ruby, e))?;
15
+
16
+ Ok(Self { engine })
13
17
  }
14
18
 
15
- fn check_permission(
19
+ /// Load the Cedar schema. Re-loading a schema drops the active policies,
20
+ /// so callers should load the schema exactly once, before any policies.
21
+ fn load_schema(ruby: &Ruby, rb_self: &Self, schema: String) -> Result<(), Error> {
22
+ rb_self
23
+ .engine
24
+ .load_schema(&schema)
25
+ .map_err(|e| map_error(ruby, e))
26
+ }
27
+
28
+ /// Replace the active policy set with the given Cedar policy source,
29
+ /// validated against the loaded schema. On error the previous policies
30
+ /// remain active.
31
+ fn load_policies(ruby: &Ruby, rb_self: &Self, policies: String) -> Result<(), Error> {
32
+ rb_self
33
+ .engine
34
+ .load_policies(&policies)
35
+ .map_err(|e| map_error(ruby, e))
36
+ }
37
+
38
+ fn check_action(
16
39
  ruby: &Ruby,
17
- rb_self: &CheckPermission,
40
+ rb_self: &Self,
18
41
  proto_bytes: magnus::RString,
19
42
  ) -> Result<magnus::RHash, Error> {
20
43
  let bytes = unsafe { proto_bytes.as_slice() };
21
44
  let result = rb_self
22
45
  .engine
23
- .check_permission(bytes)
46
+ .check(bytes)
24
47
  .map_err(|e| map_error(ruby, e))?;
25
48
  let hash = ruby.hash_new();
26
49
  hash.aset(ruby.sym_new("allowed"), result.allowed)?;
@@ -29,10 +52,93 @@ impl CheckPermission {
29
52
  }
30
53
  }
31
54
 
55
+ /// Thin 1:1 wrapper around `glaz_module::GovernPolicyCheckEngine`, the Rego
56
+ /// governance evaluator. Stateless between calls; both methods take an
57
+ /// encoded `glaz.govern.v1.EvaluateGovernPolicyRequest`. `evaluate` raises
58
+ /// on failure (matching `PermissionCheckEngine`'s convention) and returns
59
+ /// the encoded `EvaluateGovernPolicyResponse` on success — `ArgumentError`
60
+ /// for bad policy/input, `RuntimeError` for an engine-side fault, see
61
+ /// `map_error`; `debug_evaluate` is infallible and carries failures in-band
62
+ /// in its JSON response instead, since it's a raw debugging escape hatch.
63
+ ///
64
+ /// Registered as `Glaz::Native::GovernPolicyEngine` (not
65
+ /// `Glaz::GovernPolicyEngine`) specifically to avoid colliding, in name,
66
+ /// with the polished `Gitlab::Glaz::GovernPolicyEngine` wrapper — this
67
+ /// class is raw FFI plumbing; prefer the wrapper unless you specifically
68
+ /// need `debug_evaluate`.
69
+ #[magnus::wrap(class = "Glaz::Native::GovernPolicyEngine")]
70
+ struct GovernPolicyEngine {
71
+ engine: glaz_module::GovernPolicyCheckEngine,
72
+ }
73
+
74
+ impl GovernPolicyEngine {
75
+ fn initialize() -> Self {
76
+ Self {
77
+ engine: glaz_module::GovernPolicyCheckEngine::new(),
78
+ }
79
+ }
80
+
81
+ /// Evaluate using the request's `query` field. `query` must be
82
+ /// non-blank; a blank query is reported in-band as an error rather than
83
+ /// falling back to discovery (use `evaluate` for that).
84
+ fn debug_evaluate(
85
+ ruby: &Ruby,
86
+ rb_self: &Self,
87
+ proto_bytes: magnus::RString,
88
+ ) -> Result<magnus::RString, Error> {
89
+ let bytes = unsafe { proto_bytes.as_slice() };
90
+ let response = rb_self.engine.debug_evaluate(bytes);
91
+ Ok(ruby.str_from_slice(&response))
92
+ }
93
+
94
+ /// Evaluate by auto-discovering the policy's `violation`, `deny`,
95
+ /// and/or `allow` rules. The request's `query` field is ignored.
96
+ ///
97
+ /// Raises on any evaluation failure rather than returning an in-band
98
+ /// error, matching `PermissionCheckEngine#check_action`'s convention.
99
+ /// `map_error` raises `ArgumentError` for malformed protobuf, invalid
100
+ /// policy, oversized documents, undefined results, or an unrecognized
101
+ /// result shape - all caller-fixable - and `RuntimeError` for an
102
+ /// engine-side fault (e.g. the Rego evaluation time budget was
103
+ /// exceeded), which is not.
104
+ fn evaluate(
105
+ ruby: &Ruby,
106
+ rb_self: &Self,
107
+ proto_bytes: magnus::RString,
108
+ ) -> Result<magnus::RString, Error> {
109
+ let bytes = unsafe { proto_bytes.as_slice() };
110
+ let response = rb_self
111
+ .engine
112
+ .evaluate(bytes)
113
+ .map_err(|e| map_error(ruby, e))?;
114
+ Ok(ruby.str_from_slice(&response.encode_to_vec()))
115
+ }
116
+ }
117
+
118
+ /// Return the default roles embedded in the extension at build time (parsed
119
+ /// from glaz-roles' roles/**/*.yml) as an array of hashes shaped like
120
+ /// `{ id: String, name: String, permissions: [String, ...] }`.
121
+ fn roles(ruby: &Ruby) -> Result<magnus::RArray, Error> {
122
+ let array = ruby.ary_new();
123
+ for role in glaz_roles::default_roles() {
124
+ let hash = ruby.hash_new();
125
+ hash.aset(ruby.sym_new("id"), role.id.to_string())?;
126
+ hash.aset(ruby.sym_new("name"), role.name)?;
127
+ let permissions = ruby.ary_new();
128
+ for permission in &role.permissions {
129
+ permissions.push(permission.as_str())?;
130
+ }
131
+ hash.aset(ruby.sym_new("permissions"), permissions)?;
132
+ array.push(hash)?;
133
+ }
134
+ Ok(array)
135
+ }
136
+
32
137
  fn map_error(ruby: &Ruby, e: glaz_module::GlazError) -> Error {
33
138
  use glaz_module::GlazError::*;
34
139
  match e {
35
140
  InvalidArgument(_) => Error::new(ruby.exception_arg_error(), format!("{e}")),
141
+ NotInitialized(_) => Error::new(ruby.exception_runtime_error(), format!("{e}")),
36
142
  Runtime(_) => Error::new(ruby.exception_runtime_error(), format!("{e}")),
37
143
  }
38
144
  }
@@ -40,12 +146,31 @@ fn map_error(ruby: &Ruby, e: glaz_module::GlazError) -> Error {
40
146
  #[magnus::init]
41
147
  fn init(ruby: &Ruby) -> Result<(), Error> {
42
148
  let glaz = ruby.define_module("Glaz")?;
43
- let engine = glaz.define_module("Engine")?;
44
- let class = engine.define_class("CheckPermission", ruby.class_object())?;
45
- class.define_singleton_method("new", magnus::function!(CheckPermission::initialize, 0))?;
149
+ glaz.define_singleton_method("roles", magnus::function!(roles, 0))?;
150
+ let class = glaz.define_class("PermissionCheckEngine", ruby.class_object())?;
151
+ class.define_singleton_method(
152
+ "new",
153
+ magnus::function!(PermissionCheckEngine::initialize, 0),
154
+ )?;
155
+ class.define_method(
156
+ "load_schema",
157
+ magnus::method!(PermissionCheckEngine::load_schema, 1),
158
+ )?;
159
+ class.define_method(
160
+ "load_policies",
161
+ magnus::method!(PermissionCheckEngine::load_policies, 1),
162
+ )?;
46
163
  class.define_method(
47
- "check_permission",
48
- magnus::method!(CheckPermission::check_permission, 1),
164
+ "check_action",
165
+ magnus::method!(PermissionCheckEngine::check_action, 1),
166
+ )?;
167
+ let native = glaz.define_module("Native")?;
168
+ let govern = native.define_class("GovernPolicyEngine", ruby.class_object())?;
169
+ govern.define_singleton_method("new", magnus::function!(GovernPolicyEngine::initialize, 0))?;
170
+ govern.define_method(
171
+ "debug_evaluate",
172
+ magnus::method!(GovernPolicyEngine::debug_evaluate, 1),
49
173
  )?;
174
+ govern.define_method("evaluate", magnus::method!(GovernPolicyEngine::evaluate, 1))?;
50
175
  Ok(())
51
176
  }
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Gitlab
6
+ module Glaz
7
+ # Wraps a native Glaz::PermissionCheckEngine instance.
8
+ #
9
+ # The Cedar schema is fixed for the lifetime of the engine: it is loaded
10
+ # exactly once at construction and cannot be re-loaded; build a new Engine
11
+ # to change it. The policy set can be replaced at any time with
12
+ # #load_policies.
13
+ #
14
+ # Schemas must declare `User` and `Resource` entity types and declare
15
+ # each checkable action with `appliesTo { principal: [User], resource:
16
+ # [Resource], ... }`, because the engine builds `User::"<subject_uuid>"` and
17
+ # `Resource::"<object_uuid>"` entity UIDs for every check. The engine also
18
+ # injects `permissions` (a Set<Action> containing the checked action)
19
+ # into the Cedar context, so an action's declared context type must include
20
+ # `permissions: Set<Action>` alongside any custom attributes.
21
+ class Engine
22
+ # @param schema [String] Cedar schema source (`.cedarschema` DSL).
23
+ # @param policies [String] Cedar policy source, validated against the
24
+ # schema; ArgumentError is raised when either does not parse or the
25
+ # policies do not validate.
26
+ def initialize(schema:, policies:)
27
+ @native = ::Glaz::PermissionCheckEngine.new
28
+ @native.load_schema(schema)
29
+ @native.load_policies(policies)
30
+ end
31
+
32
+ # Replace the active policy set with the given Cedar policy source,
33
+ # validated against the engine's schema. Raises ArgumentError when the
34
+ # source does not parse or validate; the previous policies then remain
35
+ # active. The swap is atomic with respect to concurrent checks.
36
+ def load_policies(source)
37
+ @native.load_policies(source)
38
+ end
39
+
40
+ # Evaluate whether the subject may perform +action+ on the object.
41
+ #
42
+ # @param context [Hash] ABAC attributes made available to policies as the
43
+ # Cedar context; must match the context type the schema declares for
44
+ # the checked action.
45
+ # @return [Hash] { allowed: Boolean, reason: String }
46
+ def check_action(subject_uuid:, object_uuid:, action:, context: {})
47
+ request = ::Glaz::CheckRequest.encode(
48
+ ::Glaz::CheckRequest.new(
49
+ subject: ::Relationships::V1::Principal.new(id: subject_uuid),
50
+ action: action,
51
+ object: ::Relationships::V1::Object.new(id: object_uuid),
52
+ context: context.to_json
53
+ )
54
+ )
55
+
56
+ @native.check_action(request)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Gitlab
6
+ module Glaz
7
+ # Wraps a native Glaz::Native::GovernPolicyEngine instance - the Rego
8
+ # governance policy evaluator.
9
+ #
10
+ # The evaluator is a pure function over its arguments: it performs no
11
+ # I/O, holds no state between calls, and executes no actions. The caller
12
+ # supplies the policy's Rego source and the assembled context document,
13
+ # and is responsible for executing the returned actions.
14
+ #
15
+ # +reasons+ and +actions+ are independent, not 1:1: +reasons+ reports why
16
+ # the policy matched - one entry per `violation`/`deny` element the
17
+ # policy produced; empty means it did not match - while +actions+
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.
24
+ #
25
+ # #evaluate is the primary path: it always auto-discovers the policy's
26
+ # `violation`/`deny`/`allow` rules (the native +evaluate+ path), returns
27
+ # the hardened Hash shape described below on success, and raises on any
28
+ # evaluation failure - the native call itself raises ArgumentError for
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.
35
+ class GovernPolicyEngine
36
+ def initialize
37
+ @native = ::Glaz::Native::GovernPolicyEngine.new
38
+ end
39
+
40
+ # Evaluate a Rego governance policy against context.
41
+ #
42
+ # @param policy_rego [String] Rego policy text (max 64 KiB).
43
+ # @param context [Hash] the host-assembled context, passed to the
44
+ # policy as the Rego `input` document (max 1 MiB as JSON).
45
+ # @param data [Hash] optional precomputed Rego `data` document (e.g.
46
+ # cached settings or scan results). Top-level keys must not collide
47
+ # with the policy's package path.
48
+ # @raise [ArgumentError] for a caller-fixable evaluation failure -
49
+ # malformed policy, oversized documents, an unrecognized
50
+ # violation/deny/allow shape, and so on.
51
+ # @raise [RuntimeError] for an engine-side fault, e.g. the Rego
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: {})
67
+ encoded = ::Glaz::Govern::V1::EvaluateGovernPolicyRequest.encode(
68
+ ::Glaz::Govern::V1::EvaluateGovernPolicyRequest.new(
69
+ policy_rego: policy_rego,
70
+ input_json: context.to_json,
71
+ data_json: data.nil? || data.empty? ? "" : data.to_json
72
+ )
73
+ )
74
+
75
+ response = ::Glaz::Govern::V1::EvaluateGovernPolicyResponse.decode(
76
+ @native.evaluate(encoded)
77
+ )
78
+
79
+ {
80
+ matched: response.matched,
81
+ reasons: response.reasons.map { |reason| reason_hash(reason) },
82
+ actions: response.actions.map { |action| action_hash(action) }
83
+ }
84
+ end
85
+
86
+ # Evaluate a Rego query directly, with no `violation`/`deny`/`allow`
87
+ # interpretation. A debugging/testing escape hatch for exercising a
88
+ # policy's Rego directly; prefer #evaluate for the primary path.
89
+ # Unlike #evaluate, this does not raise on failure - +error+ stays
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.
93
+ #
94
+ # @param policy_rego [String] Rego policy text (max 64 KiB).
95
+ # @param context [Hash] the host-assembled context, passed to the
96
+ # policy as the Rego `input` document (max 1 MiB as JSON).
97
+ # @param query [String] Rego query to evaluate (e.g.
98
+ # "data.mypackage.violation"); must not be blank.
99
+ # @param data [Hash] optional precomputed Rego `data` document.
100
+ # @return [Hash] `{ result: Object, error: String | nil }` - `result`
101
+ # is exactly whatever `query` evaluated to (parsed JSON), with no
102
+ # guarantee about its shape. A non-nil `error` means evaluation
103
+ # failed; `result` is then `nil`.
104
+ def debug_evaluate(policy_rego:, context:, query:, data: {})
105
+ encoded = ::Glaz::Govern::V1::EvaluateGovernPolicyDebugRequest.encode(
106
+ ::Glaz::Govern::V1::EvaluateGovernPolicyDebugRequest.new(
107
+ policy_rego: policy_rego,
108
+ query: query.to_s,
109
+ input_json: context.to_json,
110
+ data_json: data.nil? || data.empty? ? "" : data.to_json
111
+ )
112
+ )
113
+
114
+ response = JSON.parse(@native.debug_evaluate(encoded), symbolize_names: true)
115
+
116
+ {
117
+ result: response[:result],
118
+ error: response[:error].nil? || response[:error].empty? ? nil : response[:error]
119
+ }
120
+ end
121
+
122
+ private
123
+
124
+ def action_hash(action)
125
+ {
126
+ action_type: action.action_type,
127
+ message: action.message.empty? ? nil : action.message,
128
+ params: JSON.parse(action.params_json)
129
+ }
130
+ end
131
+
132
+ def reason_hash(reason)
133
+ {
134
+ message: reason.message.empty? ? nil : reason.message,
135
+ details: JSON.parse(reason.details_json)
136
+ }
137
+ end
138
+ end
139
+ end
140
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  module Glaz
5
- VERSION = "0.0.3"
5
+ VERSION = "1.1.0"
6
6
  end
7
7
  end
data/lib/gitlab/glaz.rb CHANGED
@@ -3,24 +3,33 @@
3
3
  require_relative "glaz/version"
4
4
  require_relative 'glaz/loader'
5
5
  require "proto/service_pb"
6
+ require "proto/govern_pb"
6
7
 
7
8
  load_rust_extension
8
9
 
10
+ require_relative "glaz/engine"
11
+ require_relative "glaz/govern_policy_engine"
12
+
9
13
  module Gitlab
10
14
  module Glaz
11
15
  module_function
12
16
 
13
- def check_permission(subject_uuid:, object_uuid:, permission:, context: {})
14
- request = ::Glaz::CheckPermissionRequest.encode(
15
- ::Glaz::CheckPermissionRequest.new(
16
- subject: subject_uuid,
17
- object: object_uuid,
18
- permission: permission,
19
- context: context.to_json
20
- )
21
- )
17
+ # Returns the default roles embedded in the native extension at build time,
18
+ # sourced from the glaz-roles crate's roles/**/*.yml definitions. Each entry
19
+ # is a hash: { id: String, name: String, permissions: Array<String> }.
20
+ def roles
21
+ @roles ||= ::Glaz.roles.each do |role|
22
+ role[:permissions].freeze
23
+ role.freeze
24
+ end.freeze
25
+ end
22
26
 
23
- ::Glaz::Engine::CheckPermission.new.check_permission(request)
27
+ # Returns the shared Gitlab::Glaz::GovernPolicyEngine instance, memoized
28
+ # across calls. Evaluate Rego governance policies via
29
+ # +govern_policy_engine.evaluate+; see GovernPolicyEngine#evaluate for
30
+ # the full contract.
31
+ def govern_policy_engine
32
+ @govern_policy_engine ||= GovernPolicyEngine.new
24
33
  end
25
34
  end
26
35
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: proto/govern.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+
8
+ descriptor_data = "\n\x12proto/govern.proto\x12\x0eglaz.govern.v1\"Y\n\x1b\x45valuateGovernPolicyRequest\x12\x13\n\x0bpolicy_rego\x18\x01 \x01(\t\x12\x12\n\ninput_json\x18\x02 \x01(\t\x12\x11\n\tdata_json\x18\x03 \x01(\t\"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\"I\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\"5\n\x0cGovernReason\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x14\n\x0c\x64\x65tails_json\x18\x02 \x01(\t\"\x8d\x01\n\x1c\x45valuateGovernPolicyResponse\x12-\n\x07\x61\x63tions\x18\x01 \x03(\x0b\x32\x1c.glaz.govern.v1.GovernAction\x12-\n\x07reasons\x18\x02 \x03(\x0b\x32\x1c.glaz.govern.v1.GovernReason\x12\x0f\n\x07matched\x18\x03 \x01(\x08\x62\x06proto3"
9
+
10
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
11
+ pool.add_serialized_file(descriptor_data)
12
+
13
+ module Glaz
14
+ module Govern
15
+ module V1
16
+ EvaluateGovernPolicyRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.EvaluateGovernPolicyRequest").msgclass
17
+ EvaluateGovernPolicyDebugRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.EvaluateGovernPolicyDebugRequest").msgclass
18
+ GovernAction = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("glaz.govern.v1.GovernAction").msgclass
19
+ 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
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: proto/relationships/relationships.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'google/protobuf/timestamp_pb'
8
+
9
+
10
+ descriptor_data = "\n\'proto/relationships/relationships.proto\x12\x10relationships.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x14\n\x06Object\x12\n\n\x02id\x18\x01 \x01(\t\"\x17\n\tPrincipal\x12\n\n\x02id\x18\x01 \x01(\t\"\x12\n\x04Role\x12\n\n\x02id\x18\x01 \x01(\t\"Y\n\x08Identity\x12(\n\x06origin\x18\x01 \x01(\x0e\x32\x18.relationships.v1.Origin\x12\x11\n\torigin_id\x18\x02 \x01(\t\x12\x10\n\x08local_id\x18\x03 \x01(\t\"q\n\x07Subject\x12.\n\x08identity\x18\x01 \x01(\x0b\x32\x1a.relationships.v1.IdentityH\x00\x12\x30\n\tprincipal\x18\x02 \x01(\x0b\x32\x1b.relationships.v1.PrincipalH\x00\x42\x04\n\x02id\"\xdf\x01\n\x0cRelationship\x12*\n\x07subject\x18\x01 \x01(\x0b\x32\x19.relationships.v1.Subject\x12(\n\x06object\x18\x02 \x01(\x0b\x32\x18.relationships.v1.Object\x12$\n\x04kind\x18\x03 \x01(\x0e\x32\x16.relationships.v1.Kind\x12$\n\x04role\x18\x04 \x01(\x0b\x32\x16.relationships.v1.Role\x12-\n\ttimestamp\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xb5\x01\n\x11RelationshipInput\x12*\n\x07subject\x18\x01 \x01(\x0b\x32\x19.relationships.v1.Subject\x12(\n\x06object\x18\x02 \x01(\x0b\x32\x18.relationships.v1.Object\x12$\n\x04kind\x18\x03 \x01(\x0e\x32\x16.relationships.v1.Kind\x12$\n\x04role\x18\x04 \x01(\x0b\x32\x16.relationships.v1.Role\"\x8d\x01\n\x0fRelationshipKey\x12*\n\x07subject\x18\x01 \x01(\x0b\x32\x19.relationships.v1.Subject\x12(\n\x06object\x18\x02 \x01(\x0b\x32\x18.relationships.v1.Object\x12$\n\x04kind\x18\x03 \x01(\x0e\x32\x16.relationships.v1.Kind*1\n\x04Kind\x12\x14\n\x10KIND_UNSPECIFIED\x10\x00\x12\x13\n\x0fKIND_ASSIGNMENT\x10\x01*N\n\x06Origin\x12\x16\n\x12ORIGIN_UNSPECIFIED\x10\x00\x12\x0f\n\x0bORIGIN_SELF\x10\x01\x12\x1b\n\x17ORIGIN_GITLAB_FEDERATED\x10\x02\x42\x34Z2gitlab.com/gitlab-org/auth/iam/proto/relationshipsb\x06proto3"
11
+
12
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
+ pool.add_serialized_file(descriptor_data)
14
+
15
+ module Relationships
16
+ module V1
17
+ Object = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("relationships.v1.Object").msgclass
18
+ Principal = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("relationships.v1.Principal").msgclass
19
+ Role = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("relationships.v1.Role").msgclass
20
+ Identity = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("relationships.v1.Identity").msgclass
21
+ Subject = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("relationships.v1.Subject").msgclass
22
+ Relationship = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("relationships.v1.Relationship").msgclass
23
+ RelationshipInput = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("relationships.v1.RelationshipInput").msgclass
24
+ RelationshipKey = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("relationships.v1.RelationshipKey").msgclass
25
+ Kind = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("relationships.v1.Kind").enummodule
26
+ Origin = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("relationships.v1.Origin").enummodule
27
+ end
28
+ end