opa-golib-python-bindings 0.4.0__tar.gz → 0.4.2__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.2}/PKG-INFO +24 -1
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/README.md +23 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/go/bridge.go +54 -17
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/go/builtins.go +6 -2
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/go/compile.go +50 -12
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/pyproject.toml +1 -1
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/src/opa_bindings/_native.py +4 -3
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/src/opa_bindings/engine.py +88 -37
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/.gitignore +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/LICENSE +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/Makefile +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/go/bridge_call.c +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/go/go.mod +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/go/go.sum +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/go/merge.go +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/go/trace.go +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/hatch_build.py +0 -0
- {opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/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.2
|
|
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"
|
|
@@ -92,6 +93,18 @@ func errorJSON(code, msg string) *C.char {
|
|
|
92
93
|
return C.CString(string(b))
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
// guard converts a panic in fn into a JSON error instead of letting it kill
|
|
97
|
+
// the whole host process. Panics may fire while e.mu is held; deferred
|
|
98
|
+
// unlocks still run during unwinding, so the engine stays usable.
|
|
99
|
+
func guard(fn func() *C.char) (r *C.char) {
|
|
100
|
+
defer func() {
|
|
101
|
+
if rec := recover(); rec != nil {
|
|
102
|
+
r = errorJSON("panic", fmt.Sprint(rec))
|
|
103
|
+
}
|
|
104
|
+
}()
|
|
105
|
+
return fn()
|
|
106
|
+
}
|
|
107
|
+
|
|
95
108
|
//export OpaNew
|
|
96
109
|
func OpaNew() C.ulonglong {
|
|
97
110
|
registryMu.Lock()
|
|
@@ -107,6 +120,7 @@ func OpaNew() C.ulonglong {
|
|
|
107
120
|
|
|
108
121
|
//export OpaDestroy
|
|
109
122
|
func OpaDestroy(h C.ulonglong) {
|
|
123
|
+
defer func() { recover() }() // keep the host alive on any internal panic
|
|
110
124
|
registryMu.Lock()
|
|
111
125
|
defer registryMu.Unlock()
|
|
112
126
|
delete(registry, uint64(h))
|
|
@@ -114,11 +128,16 @@ func OpaDestroy(h C.ulonglong) {
|
|
|
114
128
|
|
|
115
129
|
//export OpaFreeString
|
|
116
130
|
func OpaFreeString(s *C.char) {
|
|
131
|
+
defer func() { recover() }()
|
|
117
132
|
C.free(unsafe.Pointer(s))
|
|
118
133
|
}
|
|
119
134
|
|
|
120
135
|
//export OpaAddPolicy
|
|
121
136
|
func OpaAddPolicy(h C.ulonglong, path, src *C.char) *C.char {
|
|
137
|
+
return guard(func() *C.char { return opaAddPolicy(h, path, src) })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func opaAddPolicy(h C.ulonglong, path, src *C.char) *C.char {
|
|
122
141
|
e, err := getEngine(h)
|
|
123
142
|
if err != nil {
|
|
124
143
|
return errorJSON("invalid_handle", err.Error())
|
|
@@ -136,6 +155,10 @@ func OpaAddPolicy(h C.ulonglong, path, src *C.char) *C.char {
|
|
|
136
155
|
|
|
137
156
|
//export OpaAddData
|
|
138
157
|
func OpaAddData(h C.ulonglong, dataPath, jsonValue *C.char) *C.char {
|
|
158
|
+
return guard(func() *C.char { return opaAddData(h, dataPath, jsonValue) })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func opaAddData(h C.ulonglong, dataPath, jsonValue *C.char) *C.char {
|
|
139
162
|
e, err := getEngine(h)
|
|
140
163
|
if err != nil {
|
|
141
164
|
return errorJSON("invalid_handle", err.Error())
|
|
@@ -170,6 +193,10 @@ func OpaAddData(h C.ulonglong, dataPath, jsonValue *C.char) *C.char {
|
|
|
170
193
|
|
|
171
194
|
//export OpaRegisterBuiltin
|
|
172
195
|
func OpaRegisterBuiltin(h C.ulonglong, name *C.char, arity C.int, cb C.opa_callback) *C.char {
|
|
196
|
+
return guard(func() *C.char { return opaRegisterBuiltin(h, name, arity, cb) })
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
func opaRegisterBuiltin(h C.ulonglong, name *C.char, arity C.int, cb C.opa_callback) *C.char {
|
|
173
200
|
e, err := getEngine(h)
|
|
174
201
|
if err != nil {
|
|
175
202
|
return errorJSON("invalid_handle", err.Error())
|
|
@@ -189,17 +216,21 @@ func OpaRegisterBuiltin(h C.ulonglong, name *C.char, arity C.int, cb C.opa_callb
|
|
|
189
216
|
|
|
190
217
|
//export OpaEvalQuery
|
|
191
218
|
func OpaEvalQuery(h C.ulonglong, query, inputJson *C.char, coverage, trace C.int) *C.char {
|
|
192
|
-
return
|
|
219
|
+
return guard(func() *C.char {
|
|
220
|
+
return evalCommon(h, C.GoString(query), inputJson, coverage != 0, trace != 0)
|
|
221
|
+
})
|
|
193
222
|
}
|
|
194
223
|
|
|
195
224
|
//export OpaEvalDocument
|
|
196
225
|
func OpaEvalDocument(h C.ulonglong, docPath, inputJson *C.char, coverage, trace C.int) *C.char {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
226
|
+
return guard(func() *C.char {
|
|
227
|
+
p := C.GoString(docPath)
|
|
228
|
+
q := "data"
|
|
229
|
+
if p != "" {
|
|
230
|
+
q = "data." + p
|
|
231
|
+
}
|
|
232
|
+
return evalCommon(h, q, inputJson, coverage != 0, trace != 0)
|
|
233
|
+
})
|
|
203
234
|
}
|
|
204
235
|
|
|
205
236
|
func evalCommon(h C.ulonglong, query string, inputJson *C.char, coverage, trace bool) *C.char {
|
|
@@ -207,9 +238,12 @@ func evalCommon(h C.ulonglong, query string, inputJson *C.char, coverage, trace
|
|
|
207
238
|
if err != nil {
|
|
208
239
|
return errorJSON("invalid_handle", err.Error())
|
|
209
240
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
241
|
+
pq, err := func() (rego.PreparedEvalQuery, error) {
|
|
242
|
+
e.mu.Lock()
|
|
243
|
+
defer e.mu.Unlock()
|
|
244
|
+
if pq, ok := e.prepared[query]; ok {
|
|
245
|
+
return pq, nil
|
|
246
|
+
}
|
|
213
247
|
opts := []func(*rego.Rego){
|
|
214
248
|
rego.Query(query),
|
|
215
249
|
rego.Store(inmem.NewFromObject(e.data)),
|
|
@@ -222,14 +256,17 @@ func evalCommon(h C.ulonglong, query string, inputJson *C.char, coverage, trace
|
|
|
222
256
|
for _, b := range e.builtins {
|
|
223
257
|
opts = append(opts, makeBuiltin(uint64(h), b))
|
|
224
258
|
}
|
|
225
|
-
pq, err
|
|
259
|
+
pq, err := rego.New(opts...).PrepareForEval(context.Background())
|
|
226
260
|
if err != nil {
|
|
227
|
-
|
|
228
|
-
return
|
|
261
|
+
var zero rego.PreparedEvalQuery
|
|
262
|
+
return zero, err
|
|
229
263
|
}
|
|
230
264
|
e.prepared[query] = pq
|
|
265
|
+
return pq, nil
|
|
266
|
+
}()
|
|
267
|
+
if err != nil {
|
|
268
|
+
return errorJSON("prepare_error", err.Error())
|
|
231
269
|
}
|
|
232
|
-
e.mu.Unlock()
|
|
233
270
|
|
|
234
271
|
evalOpts := []rego.EvalOption{}
|
|
235
272
|
if inputJson != nil {
|
|
@@ -62,12 +62,16 @@ func makeBuiltin(handle uint64, spec builtinSpec) func(*rego.Rego) {
|
|
|
62
62
|
if ret == nil {
|
|
63
63
|
return nil, fmt.Errorf("%s: callback returned NULL", spec.name)
|
|
64
64
|
}
|
|
65
|
-
// The
|
|
65
|
+
// The callback mallocs the response buffer and transfers ownership
|
|
66
|
+
// to us: copy the string out, then free with the C allocator via
|
|
67
|
+
// OpaFreeString. Nothing on the host side retains the pointer.
|
|
68
|
+
s := C.GoString(ret)
|
|
69
|
+
C.free(unsafe.Pointer(ret))
|
|
66
70
|
var envelope struct {
|
|
67
71
|
Result *json.RawMessage `json:"result"`
|
|
68
72
|
Error *string `json:"error"`
|
|
69
73
|
}
|
|
70
|
-
if err := json.Unmarshal([]byte(
|
|
74
|
+
if err := json.Unmarshal([]byte(s), &envelope); err != nil {
|
|
71
75
|
return nil, fmt.Errorf("%s: invalid callback response: %w", spec.name, err)
|
|
72
76
|
}
|
|
73
77
|
if envelope.Error != nil {
|
|
@@ -8,6 +8,7 @@ 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"
|
|
@@ -23,6 +24,12 @@ import (
|
|
|
23
24
|
//
|
|
24
25
|
//export OpaCompileFilters
|
|
25
26
|
func OpaCompileFilters(h C.ulonglong, query, inputJson, unknownsJson, target, dialect, mappingsJson, maskRule *C.char) *C.char {
|
|
27
|
+
return guard(func() *C.char {
|
|
28
|
+
return opaCompileFilters(h, query, inputJson, unknownsJson, target, dialect, mappingsJson, maskRule)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func opaCompileFilters(h C.ulonglong, query, inputJson, unknownsJson, target, dialect, mappingsJson, maskRule *C.char) *C.char {
|
|
26
33
|
e, err := getEngine(h)
|
|
27
34
|
if err != nil {
|
|
28
35
|
return errorJSON("invalid_handle", err.Error())
|
|
@@ -56,6 +63,9 @@ func OpaCompileFilters(h C.ulonglong, query, inputJson, unknownsJson, target, di
|
|
|
56
63
|
if err := json.Unmarshal([]byte(s), &mappings); err != nil {
|
|
57
64
|
return errorJSON("invalid_json", err.Error())
|
|
58
65
|
}
|
|
66
|
+
if err := validateMappings(mappings); err != nil {
|
|
67
|
+
return errorJSON("invalid_json", err.Error())
|
|
68
|
+
}
|
|
59
69
|
copts = append(copts, regocompile.Mappings(mappings))
|
|
60
70
|
}
|
|
61
71
|
|
|
@@ -67,18 +77,21 @@ func OpaCompileFilters(h C.ulonglong, query, inputJson, unknownsJson, target, di
|
|
|
67
77
|
copts = append(copts, regocompile.MaskRule(ref))
|
|
68
78
|
}
|
|
69
79
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
rego.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
ropts := func() []func(*rego.Rego) {
|
|
81
|
+
e.mu.Lock()
|
|
82
|
+
defer e.mu.Unlock()
|
|
83
|
+
ropts := []func(*rego.Rego){
|
|
84
|
+
rego.Store(inmem.NewFromObject(e.data)),
|
|
85
|
+
rego.StrictBuiltinErrors(true),
|
|
86
|
+
}
|
|
87
|
+
for path, src := range e.modules {
|
|
88
|
+
ropts = append(ropts, rego.Module(path, src))
|
|
89
|
+
}
|
|
90
|
+
for _, b := range e.builtins {
|
|
91
|
+
ropts = append(ropts, makeBuiltin(uint64(h), b))
|
|
92
|
+
}
|
|
93
|
+
return ropts
|
|
94
|
+
}()
|
|
82
95
|
copts = append(copts, regocompile.Rego(ropts...))
|
|
83
96
|
|
|
84
97
|
prepared, err := regocompile.New(copts...).Prepare(context.Background())
|
|
@@ -104,3 +117,28 @@ func OpaCompileFilters(h C.ulonglong, query, inputJson, unknownsJson, target, di
|
|
|
104
117
|
f := filters.For(tgt, dia)
|
|
105
118
|
return resultJSON(map[string]any{"query": f.Query, "masks": f.Masks})
|
|
106
119
|
}
|
|
120
|
+
|
|
121
|
+
// validateMappings enforces the shape OPA's translator assumes before the
|
|
122
|
+
// mappings reach unchecked type assertions there: the top level maps table
|
|
123
|
+
// (or unknown-short) names to objects; each entry maps column names to
|
|
124
|
+
// strings, optionally alongside the string-valued "$self"/"$table" keys.
|
|
125
|
+
// This returns an error instead of letting a malformed value panic and take
|
|
126
|
+
// the whole host process down.
|
|
127
|
+
func validateMappings(mappings map[string]any) error {
|
|
128
|
+
for table, tableMapping := range mappings {
|
|
129
|
+
tm, ok := tableMapping.(map[string]any)
|
|
130
|
+
if !ok {
|
|
131
|
+
return fmt.Errorf("mappings[%q]: table entry must be an object, got %T", table, tableMapping)
|
|
132
|
+
}
|
|
133
|
+
for column, columnMapping := range tm {
|
|
134
|
+
if _, ok := columnMapping.(string); ok {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
if column == "$self" || column == "$table" {
|
|
138
|
+
return fmt.Errorf("mappings[%q].%s: value must be a string, got %T", table, column, columnMapping)
|
|
139
|
+
}
|
|
140
|
+
return fmt.Errorf("mappings[%q].%q: column entry must be a string, got %T", table, column, columnMapping)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return nil
|
|
144
|
+
}
|
{opa_golib_python_bindings-0.4.0 → opa_golib_python_bindings-0.4.2}/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.2}/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.2}/src/opa_bindings/__init__.py
RENAMED
|
File without changes
|