opa-golib-python-bindings 0.4.0__tar.gz → 0.4.3__tar.gz
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.
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/PKG-INFO +24 -1
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/README.md +23 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/go/bridge.go +57 -19
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/go/builtins.go +8 -3
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/go/compile.go +52 -13
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/pyproject.toml +1 -1
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/src/opa_bindings/_native.py +4 -3
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/src/opa_bindings/engine.py +88 -37
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/.gitignore +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/LICENSE +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/Makefile +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/go/bridge_call.c +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/go/go.mod +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/go/go.sum +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/go/merge.go +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/go/trace.go +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/hatch_build.py +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/src/opa_bindings/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: opa-golib-python-bindings
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Python bindings for the OPA (Open Policy Agent) Rego engine via a Go c-shared library
|
|
5
5
|
Project-URL: Homepage, https://github.com/phi1010/opa-golib-python-bindings
|
|
6
6
|
Project-URL: Repository, https://github.com/phi1010/opa-golib-python-bindings
|
|
@@ -63,6 +63,10 @@ Notes:
|
|
|
63
63
|
|
|
64
64
|
- `add_data` deep-merges objects; identical values coexist, conflicting values raise
|
|
65
65
|
`OpaError(code="merge_conflict")` naming the conflicting path.
|
|
66
|
+
- A Go-side panic in any bridge call surfaces as `OpaError(code="panic")`
|
|
67
|
+
instead of killing the process; the engine stays usable afterwards. (The
|
|
68
|
+
bridge also validates `compile_filters(mappings=...)` values are strings
|
|
69
|
+
and rejects malformed shapes with `OpaError(code="invalid_json")`.)
|
|
66
70
|
- `register_function` infers arity from the callable's signature. A `*args` function is
|
|
67
71
|
variadic and is called from Rego with a single array argument: `many(["a", "b"])`
|
|
68
72
|
(OPA does not support variadic builtins with return values).
|
|
@@ -131,3 +135,22 @@ Notes:
|
|
|
131
135
|
- Rego `print(...)` output is captured per evaluation: set `engine.print_handler`
|
|
132
136
|
to a `callable(message, location)` to receive it (default: written to stderr);
|
|
133
137
|
`engine.last_prints` holds the `(message, location)` pairs of the last eval.
|
|
138
|
+
- **Concurrency:** an engine may be shared across threads — evals are
|
|
139
|
+
serialized by an internal re-entrant lock (builtins may evaluate on their
|
|
140
|
+
own engine). Per-eval state (`last_trace`, `last_coverage`, `last_prints`)
|
|
141
|
+
reflects the most recently *completed* eval, so with a shared engine prefer
|
|
142
|
+
return values over `last_*` attributes, or use one engine per thread.
|
|
143
|
+
- **Resource limits / DoS:** there are no built-in caps. Policy/data sizes,
|
|
144
|
+
the number of engines (call `close()`; `__del__` is best-effort under GC),
|
|
145
|
+
and the set of distinct query strings (each is cached as a prepared query
|
|
146
|
+
on the engine, invalidated by any config change) are bounded only by the
|
|
147
|
+
process. If queries are dynamic or attacker-shaped, cap and canonicalize
|
|
148
|
+
them in the application; treat `query`, `policy source`, and `data` as
|
|
149
|
+
privileged inputs on a multi-tenant host. Rego itself can loop/compute
|
|
150
|
+
without limits, so the caller should apply timeouts/load controls at the
|
|
151
|
+
application layer if policies are untrusted.
|
|
152
|
+
- **Confidentiality:** traces (`trace=True`) capture the plugged values of
|
|
153
|
+
local variables, and Rego `print()` calls see their arguments; both can
|
|
154
|
+
contain secrets from `input` or `data`. Keep them off shared logs, or
|
|
155
|
+
scrub them, when evaluating sensitive inputs. (The default print handler
|
|
156
|
+
writes to stderr.)
|
|
@@ -37,6 +37,10 @@ Notes:
|
|
|
37
37
|
|
|
38
38
|
- `add_data` deep-merges objects; identical values coexist, conflicting values raise
|
|
39
39
|
`OpaError(code="merge_conflict")` naming the conflicting path.
|
|
40
|
+
- A Go-side panic in any bridge call surfaces as `OpaError(code="panic")`
|
|
41
|
+
instead of killing the process; the engine stays usable afterwards. (The
|
|
42
|
+
bridge also validates `compile_filters(mappings=...)` values are strings
|
|
43
|
+
and rejects malformed shapes with `OpaError(code="invalid_json")`.)
|
|
40
44
|
- `register_function` infers arity from the callable's signature. A `*args` function is
|
|
41
45
|
variadic and is called from Rego with a single array argument: `many(["a", "b"])`
|
|
42
46
|
(OPA does not support variadic builtins with return values).
|
|
@@ -105,3 +109,22 @@ Notes:
|
|
|
105
109
|
- Rego `print(...)` output is captured per evaluation: set `engine.print_handler`
|
|
106
110
|
to a `callable(message, location)` to receive it (default: written to stderr);
|
|
107
111
|
`engine.last_prints` holds the `(message, location)` pairs of the last eval.
|
|
112
|
+
- **Concurrency:** an engine may be shared across threads — evals are
|
|
113
|
+
serialized by an internal re-entrant lock (builtins may evaluate on their
|
|
114
|
+
own engine). Per-eval state (`last_trace`, `last_coverage`, `last_prints`)
|
|
115
|
+
reflects the most recently *completed* eval, so with a shared engine prefer
|
|
116
|
+
return values over `last_*` attributes, or use one engine per thread.
|
|
117
|
+
- **Resource limits / DoS:** there are no built-in caps. Policy/data sizes,
|
|
118
|
+
the number of engines (call `close()`; `__del__` is best-effort under GC),
|
|
119
|
+
and the set of distinct query strings (each is cached as a prepared query
|
|
120
|
+
on the engine, invalidated by any config change) are bounded only by the
|
|
121
|
+
process. If queries are dynamic or attacker-shaped, cap and canonicalize
|
|
122
|
+
them in the application; treat `query`, `policy source`, and `data` as
|
|
123
|
+
privileged inputs on a multi-tenant host. Rego itself can loop/compute
|
|
124
|
+
without limits, so the caller should apply timeouts/load controls at the
|
|
125
|
+
application layer if policies are untrusted.
|
|
126
|
+
- **Confidentiality:** traces (`trace=True`) capture the plugged values of
|
|
127
|
+
local variables, and Rego `print()` calls see their arguments; both can
|
|
128
|
+
contain secrets from `input` or `data`. Keep them off shared logs, or
|
|
129
|
+
scrub them, when evaluating sensitive inputs. (The default print handler
|
|
130
|
+
writes to stderr.)
|
|
@@ -5,9 +5,10 @@ package main
|
|
|
5
5
|
|
|
6
6
|
// Callback provided by the host (Python). It receives the engine handle, the
|
|
7
7
|
// builtin name and the arguments as a JSON array. It returns a pointer to a
|
|
8
|
-
// JSON envelope {"result": ...} or {"error": "..."}.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// malloc'd JSON envelope {"result": ...} or {"error": "..."}. Ownership of the
|
|
9
|
+
// buffer transfers to the Go side, which copies it synchronously and frees it
|
|
10
|
+
// with the C allocator (see builtins.go). The host must not retain or reuse
|
|
11
|
+
// the pointer after the callback returns.
|
|
11
12
|
typedef char* (*opa_callback)(unsigned long long h, char* name, char* argsJson);
|
|
12
13
|
*/
|
|
13
14
|
import "C"
|
|
@@ -25,6 +26,7 @@ import (
|
|
|
25
26
|
"github.com/open-policy-agent/opa/v1/rego"
|
|
26
27
|
"github.com/open-policy-agent/opa/v1/storage/inmem"
|
|
27
28
|
"github.com/open-policy-agent/opa/v1/topdown/print"
|
|
29
|
+
"github.com/open-policy-agent/opa/v1/util"
|
|
28
30
|
)
|
|
29
31
|
|
|
30
32
|
type printMsg struct {
|
|
@@ -92,6 +94,18 @@ func errorJSON(code, msg string) *C.char {
|
|
|
92
94
|
return C.CString(string(b))
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
// guard converts a panic in fn into a JSON error instead of letting it kill
|
|
98
|
+
// the whole host process. Panics may fire while e.mu is held; deferred
|
|
99
|
+
// unlocks still run during unwinding, so the engine stays usable.
|
|
100
|
+
func guard(fn func() *C.char) (r *C.char) {
|
|
101
|
+
defer func() {
|
|
102
|
+
if rec := recover(); rec != nil {
|
|
103
|
+
r = errorJSON("panic", fmt.Sprint(rec))
|
|
104
|
+
}
|
|
105
|
+
}()
|
|
106
|
+
return fn()
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
//export OpaNew
|
|
96
110
|
func OpaNew() C.ulonglong {
|
|
97
111
|
registryMu.Lock()
|
|
@@ -107,6 +121,7 @@ func OpaNew() C.ulonglong {
|
|
|
107
121
|
|
|
108
122
|
//export OpaDestroy
|
|
109
123
|
func OpaDestroy(h C.ulonglong) {
|
|
124
|
+
defer func() { recover() }() // keep the host alive on any internal panic
|
|
110
125
|
registryMu.Lock()
|
|
111
126
|
defer registryMu.Unlock()
|
|
112
127
|
delete(registry, uint64(h))
|
|
@@ -114,11 +129,16 @@ func OpaDestroy(h C.ulonglong) {
|
|
|
114
129
|
|
|
115
130
|
//export OpaFreeString
|
|
116
131
|
func OpaFreeString(s *C.char) {
|
|
132
|
+
defer func() { recover() }()
|
|
117
133
|
C.free(unsafe.Pointer(s))
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
//export OpaAddPolicy
|
|
121
137
|
func OpaAddPolicy(h C.ulonglong, path, src *C.char) *C.char {
|
|
138
|
+
return guard(func() *C.char { return opaAddPolicy(h, path, src) })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func opaAddPolicy(h C.ulonglong, path, src *C.char) *C.char {
|
|
122
142
|
e, err := getEngine(h)
|
|
123
143
|
if err != nil {
|
|
124
144
|
return errorJSON("invalid_handle", err.Error())
|
|
@@ -136,12 +156,16 @@ func OpaAddPolicy(h C.ulonglong, path, src *C.char) *C.char {
|
|
|
136
156
|
|
|
137
157
|
//export OpaAddData
|
|
138
158
|
func OpaAddData(h C.ulonglong, dataPath, jsonValue *C.char) *C.char {
|
|
159
|
+
return guard(func() *C.char { return opaAddData(h, dataPath, jsonValue) })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func opaAddData(h C.ulonglong, dataPath, jsonValue *C.char) *C.char {
|
|
139
163
|
e, err := getEngine(h)
|
|
140
164
|
if err != nil {
|
|
141
165
|
return errorJSON("invalid_handle", err.Error())
|
|
142
166
|
}
|
|
143
167
|
var v any
|
|
144
|
-
if err :=
|
|
168
|
+
if err := util.UnmarshalJSON([]byte(C.GoString(jsonValue)), &v); err != nil {
|
|
145
169
|
return errorJSON("invalid_json", err.Error())
|
|
146
170
|
}
|
|
147
171
|
// Wrap the value in nested objects along dataPath.
|
|
@@ -170,6 +194,10 @@ func OpaAddData(h C.ulonglong, dataPath, jsonValue *C.char) *C.char {
|
|
|
170
194
|
|
|
171
195
|
//export OpaRegisterBuiltin
|
|
172
196
|
func OpaRegisterBuiltin(h C.ulonglong, name *C.char, arity C.int, cb C.opa_callback) *C.char {
|
|
197
|
+
return guard(func() *C.char { return opaRegisterBuiltin(h, name, arity, cb) })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
func opaRegisterBuiltin(h C.ulonglong, name *C.char, arity C.int, cb C.opa_callback) *C.char {
|
|
173
201
|
e, err := getEngine(h)
|
|
174
202
|
if err != nil {
|
|
175
203
|
return errorJSON("invalid_handle", err.Error())
|
|
@@ -189,17 +217,21 @@ func OpaRegisterBuiltin(h C.ulonglong, name *C.char, arity C.int, cb C.opa_callb
|
|
|
189
217
|
|
|
190
218
|
//export OpaEvalQuery
|
|
191
219
|
func OpaEvalQuery(h C.ulonglong, query, inputJson *C.char, coverage, trace C.int) *C.char {
|
|
192
|
-
return
|
|
220
|
+
return guard(func() *C.char {
|
|
221
|
+
return evalCommon(h, C.GoString(query), inputJson, coverage != 0, trace != 0)
|
|
222
|
+
})
|
|
193
223
|
}
|
|
194
224
|
|
|
195
225
|
//export OpaEvalDocument
|
|
196
226
|
func OpaEvalDocument(h C.ulonglong, docPath, inputJson *C.char, coverage, trace C.int) *C.char {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
227
|
+
return guard(func() *C.char {
|
|
228
|
+
p := C.GoString(docPath)
|
|
229
|
+
q := "data"
|
|
230
|
+
if p != "" {
|
|
231
|
+
q = "data." + p
|
|
232
|
+
}
|
|
233
|
+
return evalCommon(h, q, inputJson, coverage != 0, trace != 0)
|
|
234
|
+
})
|
|
203
235
|
}
|
|
204
236
|
|
|
205
237
|
func evalCommon(h C.ulonglong, query string, inputJson *C.char, coverage, trace bool) *C.char {
|
|
@@ -207,9 +239,12 @@ func evalCommon(h C.ulonglong, query string, inputJson *C.char, coverage, trace
|
|
|
207
239
|
if err != nil {
|
|
208
240
|
return errorJSON("invalid_handle", err.Error())
|
|
209
241
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
242
|
+
pq, err := func() (rego.PreparedEvalQuery, error) {
|
|
243
|
+
e.mu.Lock()
|
|
244
|
+
defer e.mu.Unlock()
|
|
245
|
+
if pq, ok := e.prepared[query]; ok {
|
|
246
|
+
return pq, nil
|
|
247
|
+
}
|
|
213
248
|
opts := []func(*rego.Rego){
|
|
214
249
|
rego.Query(query),
|
|
215
250
|
rego.Store(inmem.NewFromObject(e.data)),
|
|
@@ -222,21 +257,24 @@ func evalCommon(h C.ulonglong, query string, inputJson *C.char, coverage, trace
|
|
|
222
257
|
for _, b := range e.builtins {
|
|
223
258
|
opts = append(opts, makeBuiltin(uint64(h), b))
|
|
224
259
|
}
|
|
225
|
-
pq, err
|
|
260
|
+
pq, err := rego.New(opts...).PrepareForEval(context.Background())
|
|
226
261
|
if err != nil {
|
|
227
|
-
|
|
228
|
-
return
|
|
262
|
+
var zero rego.PreparedEvalQuery
|
|
263
|
+
return zero, err
|
|
229
264
|
}
|
|
230
265
|
e.prepared[query] = pq
|
|
266
|
+
return pq, nil
|
|
267
|
+
}()
|
|
268
|
+
if err != nil {
|
|
269
|
+
return errorJSON("prepare_error", err.Error())
|
|
231
270
|
}
|
|
232
|
-
e.mu.Unlock()
|
|
233
271
|
|
|
234
272
|
evalOpts := []rego.EvalOption{}
|
|
235
273
|
if inputJson != nil {
|
|
236
274
|
s := C.GoString(inputJson)
|
|
237
275
|
if s != "" {
|
|
238
276
|
var input any
|
|
239
|
-
if err :=
|
|
277
|
+
if err := util.UnmarshalJSON([]byte(s), &input); err != nil {
|
|
240
278
|
return errorJSON("invalid_json", err.Error())
|
|
241
279
|
}
|
|
242
280
|
evalOpts = append(evalOpts, rego.EvalInput(input))
|
|
@@ -15,6 +15,7 @@ import (
|
|
|
15
15
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
16
16
|
"github.com/open-policy-agent/opa/v1/rego"
|
|
17
17
|
"github.com/open-policy-agent/opa/v1/types"
|
|
18
|
+
"github.com/open-policy-agent/opa/v1/util"
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
func makeBuiltin(handle uint64, spec builtinSpec) func(*rego.Rego) {
|
|
@@ -62,12 +63,16 @@ func makeBuiltin(handle uint64, spec builtinSpec) func(*rego.Rego) {
|
|
|
62
63
|
if ret == nil {
|
|
63
64
|
return nil, fmt.Errorf("%s: callback returned NULL", spec.name)
|
|
64
65
|
}
|
|
65
|
-
// The
|
|
66
|
+
// The callback mallocs the response buffer and transfers ownership
|
|
67
|
+
// to us: copy the string out, then free with the C allocator via
|
|
68
|
+
// OpaFreeString. Nothing on the host side retains the pointer.
|
|
69
|
+
s := C.GoString(ret)
|
|
70
|
+
C.free(unsafe.Pointer(ret))
|
|
66
71
|
var envelope struct {
|
|
67
72
|
Result *json.RawMessage `json:"result"`
|
|
68
73
|
Error *string `json:"error"`
|
|
69
74
|
}
|
|
70
|
-
if err := json.Unmarshal([]byte(
|
|
75
|
+
if err := json.Unmarshal([]byte(s), &envelope); err != nil {
|
|
71
76
|
return nil, fmt.Errorf("%s: invalid callback response: %w", spec.name, err)
|
|
72
77
|
}
|
|
73
78
|
if envelope.Error != nil {
|
|
@@ -78,7 +83,7 @@ func makeBuiltin(handle uint64, spec builtinSpec) func(*rego.Rego) {
|
|
|
78
83
|
return nil, nil
|
|
79
84
|
}
|
|
80
85
|
var v any
|
|
81
|
-
if err :=
|
|
86
|
+
if err := util.UnmarshalJSON(*envelope.Result, &v); err != nil {
|
|
82
87
|
return nil, err
|
|
83
88
|
}
|
|
84
89
|
val, err := ast.InterfaceToValue(v)
|
|
@@ -8,11 +8,13 @@ import "C"
|
|
|
8
8
|
import (
|
|
9
9
|
"context"
|
|
10
10
|
"encoding/json"
|
|
11
|
+
"fmt"
|
|
11
12
|
|
|
12
13
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
13
14
|
"github.com/open-policy-agent/opa/v1/rego"
|
|
14
15
|
regocompile "github.com/open-policy-agent/opa/v1/rego/compile"
|
|
15
16
|
"github.com/open-policy-agent/opa/v1/storage/inmem"
|
|
17
|
+
"github.com/open-policy-agent/opa/v1/util"
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
// OpaCompileFilters partially evaluates a query with respect to the given
|
|
@@ -23,6 +25,12 @@ import (
|
|
|
23
25
|
//
|
|
24
26
|
//export OpaCompileFilters
|
|
25
27
|
func OpaCompileFilters(h C.ulonglong, query, inputJson, unknownsJson, target, dialect, mappingsJson, maskRule *C.char) *C.char {
|
|
28
|
+
return guard(func() *C.char {
|
|
29
|
+
return opaCompileFilters(h, query, inputJson, unknownsJson, target, dialect, mappingsJson, maskRule)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func opaCompileFilters(h C.ulonglong, query, inputJson, unknownsJson, target, dialect, mappingsJson, maskRule *C.char) *C.char {
|
|
26
34
|
e, err := getEngine(h)
|
|
27
35
|
if err != nil {
|
|
28
36
|
return errorJSON("invalid_handle", err.Error())
|
|
@@ -56,6 +64,9 @@ func OpaCompileFilters(h C.ulonglong, query, inputJson, unknownsJson, target, di
|
|
|
56
64
|
if err := json.Unmarshal([]byte(s), &mappings); err != nil {
|
|
57
65
|
return errorJSON("invalid_json", err.Error())
|
|
58
66
|
}
|
|
67
|
+
if err := validateMappings(mappings); err != nil {
|
|
68
|
+
return errorJSON("invalid_json", err.Error())
|
|
69
|
+
}
|
|
59
70
|
copts = append(copts, regocompile.Mappings(mappings))
|
|
60
71
|
}
|
|
61
72
|
|
|
@@ -67,18 +78,21 @@ func OpaCompileFilters(h C.ulonglong, query, inputJson, unknownsJson, target, di
|
|
|
67
78
|
copts = append(copts, regocompile.MaskRule(ref))
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
rego.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
ropts := func() []func(*rego.Rego) {
|
|
82
|
+
e.mu.Lock()
|
|
83
|
+
defer e.mu.Unlock()
|
|
84
|
+
ropts := []func(*rego.Rego){
|
|
85
|
+
rego.Store(inmem.NewFromObject(e.data)),
|
|
86
|
+
rego.StrictBuiltinErrors(true),
|
|
87
|
+
}
|
|
88
|
+
for path, src := range e.modules {
|
|
89
|
+
ropts = append(ropts, rego.Module(path, src))
|
|
90
|
+
}
|
|
91
|
+
for _, b := range e.builtins {
|
|
92
|
+
ropts = append(ropts, makeBuiltin(uint64(h), b))
|
|
93
|
+
}
|
|
94
|
+
return ropts
|
|
95
|
+
}()
|
|
82
96
|
copts = append(copts, regocompile.Rego(ropts...))
|
|
83
97
|
|
|
84
98
|
prepared, err := regocompile.New(copts...).Prepare(context.Background())
|
|
@@ -90,7 +104,7 @@ func OpaCompileFilters(h C.ulonglong, query, inputJson, unknownsJson, target, di
|
|
|
90
104
|
if inputJson != nil {
|
|
91
105
|
if s := C.GoString(inputJson); s != "" {
|
|
92
106
|
var input any
|
|
93
|
-
if err :=
|
|
107
|
+
if err := util.UnmarshalJSON([]byte(s), &input); err != nil {
|
|
94
108
|
return errorJSON("invalid_json", err.Error())
|
|
95
109
|
}
|
|
96
110
|
evalOpts = append(evalOpts, rego.EvalInput(input))
|
|
@@ -104,3 +118,28 @@ func OpaCompileFilters(h C.ulonglong, query, inputJson, unknownsJson, target, di
|
|
|
104
118
|
f := filters.For(tgt, dia)
|
|
105
119
|
return resultJSON(map[string]any{"query": f.Query, "masks": f.Masks})
|
|
106
120
|
}
|
|
121
|
+
|
|
122
|
+
// validateMappings enforces the shape OPA's translator assumes before the
|
|
123
|
+
// mappings reach unchecked type assertions there: the top level maps table
|
|
124
|
+
// (or unknown-short) names to objects; each entry maps column names to
|
|
125
|
+
// strings, optionally alongside the string-valued "$self"/"$table" keys.
|
|
126
|
+
// This returns an error instead of letting a malformed value panic and take
|
|
127
|
+
// the whole host process down.
|
|
128
|
+
func validateMappings(mappings map[string]any) error {
|
|
129
|
+
for table, tableMapping := range mappings {
|
|
130
|
+
tm, ok := tableMapping.(map[string]any)
|
|
131
|
+
if !ok {
|
|
132
|
+
return fmt.Errorf("mappings[%q]: table entry must be an object, got %T", table, tableMapping)
|
|
133
|
+
}
|
|
134
|
+
for column, columnMapping := range tm {
|
|
135
|
+
if _, ok := columnMapping.(string); ok {
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
if column == "$self" || column == "$table" {
|
|
139
|
+
return fmt.Errorf("mappings[%q].%s: value must be a string, got %T", table, column, columnMapping)
|
|
140
|
+
}
|
|
141
|
+
return fmt.Errorf("mappings[%q].%q: column entry must be a string, got %T", table, column, columnMapping)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return nil
|
|
145
|
+
}
|
{opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/src/opa_bindings/_native.py
RENAMED
|
@@ -6,9 +6,10 @@ from pathlib import Path
|
|
|
6
6
|
_LIB_PATH = Path(__file__).parent / "libopabridge.so"
|
|
7
7
|
|
|
8
8
|
# char* (*opa_callback)(unsigned long long h, char* name, char* argsJson)
|
|
9
|
-
# Return type is c_void_p so ctypes does not copy/free
|
|
10
|
-
#
|
|
11
|
-
# it
|
|
9
|
+
# Return type is c_void_p so ctypes does not copy/free. The buffer is
|
|
10
|
+
# malloc'd by the host with the C allocator and ownership transfers to the
|
|
11
|
+
# Go side, which copies it and frees it with the same allocator; nothing on
|
|
12
|
+
# the Python side retains the pointer.
|
|
12
13
|
CALLBACK = ctypes.CFUNCTYPE(
|
|
13
14
|
ctypes.c_void_p, ctypes.c_uint64, ctypes.c_char_p, ctypes.c_char_p
|
|
14
15
|
)
|
{opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/src/opa_bindings/engine.py
RENAMED
|
@@ -4,6 +4,7 @@ import ctypes
|
|
|
4
4
|
import inspect
|
|
5
5
|
import json
|
|
6
6
|
import sys
|
|
7
|
+
import threading
|
|
7
8
|
|
|
8
9
|
from . import _native
|
|
9
10
|
|
|
@@ -21,6 +22,7 @@ class OpaUndefinedError(OpaError):
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
_lib = None
|
|
25
|
+
_libc = None
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
def _get_lib():
|
|
@@ -30,20 +32,59 @@ def _get_lib():
|
|
|
30
32
|
return _lib
|
|
31
33
|
|
|
32
34
|
|
|
35
|
+
def _get_libc():
|
|
36
|
+
"""Handle for libc's malloc: callback response buffers are owned by the
|
|
37
|
+
Go side, which frees them with the C allocator (see _dispatch)."""
|
|
38
|
+
global _libc
|
|
39
|
+
if _libc is None:
|
|
40
|
+
_libc = ctypes.CDLL(None)
|
|
41
|
+
_libc.malloc.restype = ctypes.c_void_p
|
|
42
|
+
_libc.malloc.argtypes = [ctypes.c_size_t]
|
|
43
|
+
return _libc
|
|
44
|
+
|
|
45
|
+
|
|
33
46
|
class OpaEngine:
|
|
34
47
|
"""An embedded OPA policy engine instance.
|
|
35
48
|
|
|
36
49
|
Policies are Rego modules added under a path, data is deep-merged JSON,
|
|
37
50
|
and Python callables can be registered as custom Rego builtins.
|
|
51
|
+
|
|
52
|
+
Concurrency and resource notes:
|
|
53
|
+
|
|
54
|
+
- A single engine may be shared across threads: evaluations are
|
|
55
|
+
serialized by an internal re-entrant lock (which also allows a builtin
|
|
56
|
+
to evaluate on its own engine). Configuration calls (``add_policy``,
|
|
57
|
+
``add_data``, ``register_function``) may interleave with evals from
|
|
58
|
+
other threads, as in the Go engine itself.
|
|
59
|
+
- Per-eval state (``last_coverage``, ``last_trace``, ``last_prints``)
|
|
60
|
+
reflects the most recently *completed* eval; under concurrent evals on
|
|
61
|
+
a shared engine these may interleave. Use one engine per thread, or
|
|
62
|
+
rely on return values instead of the ``last_*`` attributes, if that
|
|
63
|
+
matters.
|
|
64
|
+
- There are no built-in caps on memory or CPU: policy and data size,
|
|
65
|
+
number of engines (call ``close()`` to release one), and the number of
|
|
66
|
+
distinct queries evaluated are all bounded only by the process. Each
|
|
67
|
+
distinct query string is kept in a prepared-query cache on the engine
|
|
68
|
+
(invalidated by any configuration change), so applications that
|
|
69
|
+
evaluate unbounded attacker-shaped query strings grow memory without
|
|
70
|
+
bound — keep queries a fixed, application-controlled set.
|
|
38
71
|
"""
|
|
39
72
|
|
|
40
73
|
def __init__(self):
|
|
41
74
|
self._lib = _get_lib()
|
|
75
|
+
self._libc = _get_libc()
|
|
42
76
|
self._handle = self._lib.OpaNew()
|
|
43
77
|
self._functions = {}
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
|
|
78
|
+
# Serializes evals per engine so the per-eval callback buffer list
|
|
79
|
+
# below stays consistent even when multiple threads share an engine
|
|
80
|
+
# or a builtin re-enters eval on the same thread. A plain RLock: Go
|
|
81
|
+
# callbacks for this engine's builtins run on the calling thread while
|
|
82
|
+
# it holds the lock.
|
|
83
|
+
self._eval_lock = threading.RLock()
|
|
84
|
+
# Callback responses are malloc'd with the C allocator; Go frees them
|
|
85
|
+
# with OpaFreeString after copying (ownership transfer), so Python
|
|
86
|
+
# must NOT keep references. Buffers are only alive between _dispatch
|
|
87
|
+
# and the matching OpaFreeString, which the eval lock makes safe.
|
|
47
88
|
self._trampoline = _native.CALLBACK(self._dispatch)
|
|
48
89
|
#: Called as ``print_handler(message, location)`` for each Rego
|
|
49
90
|
#: ``print(...)`` during evaluation; None writes them to stderr.
|
|
@@ -133,13 +174,20 @@ class OpaEngine:
|
|
|
133
174
|
except Exception as e: # never let an exception cross into Go
|
|
134
175
|
response = {"error": repr(e)}
|
|
135
176
|
try:
|
|
136
|
-
|
|
177
|
+
raw = json.dumps(response).encode()
|
|
137
178
|
except Exception as e:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
179
|
+
raw = json.dumps({"error": f"unserializable result: {e!r}"}).encode()
|
|
180
|
+
# malloc with the C allocator: ownership of this buffer transfers to
|
|
181
|
+
# Go, which frees it with OpaFreeString (libc free) after copying.
|
|
182
|
+
# Nothing on the Python side retains the pointer. It must not be
|
|
183
|
+
# created with ctypes buffers — those use Python's allocator, which
|
|
184
|
+
# is not compatible with libc free.
|
|
185
|
+
buf = self._libc.malloc(len(raw) + 1)
|
|
186
|
+
if not buf:
|
|
187
|
+
return None # Go reports "callback returned NULL"
|
|
188
|
+
ctypes.memmove(buf, raw, len(raw))
|
|
189
|
+
ctypes.memset(buf + len(raw), 0, 1)
|
|
190
|
+
return buf
|
|
143
191
|
|
|
144
192
|
# -- public API ----------------------------------------------------
|
|
145
193
|
|
|
@@ -197,16 +245,17 @@ class OpaEngine:
|
|
|
197
245
|
at each step, is stored in ``self.last_trace``.
|
|
198
246
|
"""
|
|
199
247
|
self._check_open()
|
|
200
|
-
self.
|
|
201
|
-
|
|
202
|
-
self.
|
|
203
|
-
self.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
248
|
+
with self._eval_lock:
|
|
249
|
+
self._eval_reset()
|
|
250
|
+
rs = self._eval_result(
|
|
251
|
+
self._call(
|
|
252
|
+
self._lib.OpaEvalQuery,
|
|
253
|
+
query.encode(),
|
|
254
|
+
_encode_input(input),
|
|
255
|
+
int(coverage),
|
|
256
|
+
int(trace),
|
|
257
|
+
)
|
|
208
258
|
)
|
|
209
|
-
)
|
|
210
259
|
if not rs:
|
|
211
260
|
return []
|
|
212
261
|
return [r.get("bindings", {}) for r in rs]
|
|
@@ -221,16 +270,17 @@ class OpaEngine:
|
|
|
221
270
|
with ``trace=True`` the event trace is stored in ``self.last_trace``.
|
|
222
271
|
"""
|
|
223
272
|
self._check_open()
|
|
224
|
-
self.
|
|
225
|
-
|
|
226
|
-
self.
|
|
227
|
-
self.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
273
|
+
with self._eval_lock:
|
|
274
|
+
self._eval_reset()
|
|
275
|
+
rs = self._eval_result(
|
|
276
|
+
self._call(
|
|
277
|
+
self._lib.OpaEvalDocument,
|
|
278
|
+
path.encode(),
|
|
279
|
+
_encode_input(input),
|
|
280
|
+
int(coverage),
|
|
281
|
+
int(trace),
|
|
282
|
+
)
|
|
232
283
|
)
|
|
233
|
-
)
|
|
234
284
|
if not rs or not rs[0].get("expressions"):
|
|
235
285
|
raise OpaUndefinedError(f"data.{path}" if path else "data")
|
|
236
286
|
return rs[0]["expressions"][0]["value"]
|
|
@@ -279,16 +329,17 @@ class OpaEngine:
|
|
|
279
329
|
is fine, as it resolves during partial evaluation.
|
|
280
330
|
"""
|
|
281
331
|
self._check_open()
|
|
282
|
-
|
|
283
|
-
self.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
332
|
+
with self._eval_lock:
|
|
333
|
+
envelope = self._call(
|
|
334
|
+
self._lib.OpaCompileFilters,
|
|
335
|
+
query.encode(),
|
|
336
|
+
_encode_input(input),
|
|
337
|
+
json.dumps(list(unknowns)).encode(),
|
|
338
|
+
target.encode(),
|
|
339
|
+
dialect.encode(),
|
|
340
|
+
b"" if mappings is None else json.dumps(mappings).encode(),
|
|
341
|
+
(mask_rule or "").encode(),
|
|
342
|
+
)
|
|
292
343
|
return envelope.get("result")
|
|
293
344
|
|
|
294
345
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.3}/src/opa_bindings/__init__.py
RENAMED
|
File without changes
|