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.
- checksums.yaml +4 -4
- data/Cargo.lock +862 -78
- data/Cargo.toml +5 -2
- data/README.md +21 -37
- data/ext/glaz/Cargo.toml +3 -0
- data/ext/glaz/src/lib.rs +139 -14
- data/lib/gitlab/glaz/3.1/glaz.so +0 -0
- data/lib/gitlab/glaz/3.2/glaz.so +0 -0
- data/lib/gitlab/glaz/3.3/glaz.so +0 -0
- data/lib/gitlab/glaz/3.4/glaz.so +0 -0
- data/lib/gitlab/glaz/4.0/glaz.so +0 -0
- data/lib/gitlab/glaz/engine.rb +60 -0
- data/lib/gitlab/glaz/govern_policy_engine.rb +140 -0
- data/lib/gitlab/glaz/version.rb +1 -1
- data/lib/gitlab/glaz.rb +19 -10
- data/lib/proto/govern_pb.rb +23 -0
- data/lib/proto/relationships/relationships_pb.rb +28 -0
- data/lib/proto/service_pb.rb +7 -5
- metadata +6 -2
data/Cargo.toml
CHANGED
|
@@ -4,11 +4,14 @@ members = ["ext/*"]
|
|
|
4
4
|
resolver = "2"
|
|
5
5
|
|
|
6
6
|
[workspace.package]
|
|
7
|
-
version = "
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
37
|
-
```
|
|
21
|
+
engine = Gitlab::Glaz::Engine.new(schema: cedar_schema, policies: cedar_policies)
|
|
38
22
|
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
36
|
+
The schema is loaded once at construction and cannot change; build a new engine to use a new schema.
|
|
49
37
|
|
|
50
|
-
|
|
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
|
-
|
|
40
|
+
## Development
|
|
53
41
|
|
|
54
42
|
```shell
|
|
55
|
-
|
|
43
|
+
bundle exec rake compile # build the native extension (required before rspec)
|
|
44
|
+
bundle exec rspec
|
|
56
45
|
```
|
|
57
46
|
|
|
58
|
-
|
|
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
|
-
|
|
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
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
|
-
|
|
4
|
-
|
|
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
|
|
12
|
+
impl PermissionCheckEngine {
|
|
9
13
|
fn initialize(ruby: &Ruby) -> Result<Self, Error> {
|
|
10
|
-
glaz_module::PermissionCheckEngine::new()
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
let engine = glaz_module::PermissionCheckEngine::new().map_err(|e| map_error(ruby, e))?;
|
|
15
|
+
|
|
16
|
+
Ok(Self { engine })
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
|
|
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: &
|
|
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
|
-
.
|
|
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
|
-
|
|
44
|
-
let class =
|
|
45
|
-
class.define_singleton_method(
|
|
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
|
-
"
|
|
48
|
-
magnus::method!(
|
|
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
|
}
|
data/lib/gitlab/glaz/3.1/glaz.so
CHANGED
|
Binary file
|
data/lib/gitlab/glaz/3.2/glaz.so
CHANGED
|
Binary file
|
data/lib/gitlab/glaz/3.3/glaz.so
CHANGED
|
Binary file
|
data/lib/gitlab/glaz/3.4/glaz.so
CHANGED
|
Binary file
|
data/lib/gitlab/glaz/4.0/glaz.so
CHANGED
|
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
|
data/lib/gitlab/glaz/version.rb
CHANGED
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|