opa-golib-python-bindings 0.1.2__tar.gz → 0.3.0__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.3.0/PKG-INFO +133 -0
- opa_golib_python_bindings-0.3.0/README.md +107 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/go/bridge.go +32 -6
- opa_golib_python_bindings-0.3.0/go/compile.go +106 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/go/go.mod +3 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/go/go.sum +11 -0
- opa_golib_python_bindings-0.3.0/go/trace.go +78 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/pyproject.toml +2 -2
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/src/opa_bindings/_native.py +26 -2
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/src/opa_bindings/engine.py +108 -6
- opa_golib_python_bindings-0.1.2/PKG-INFO +0 -73
- opa_golib_python_bindings-0.1.2/README.md +0 -48
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/.gitignore +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/LICENSE +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/Makefile +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/go/bridge_call.c +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/go/builtins.go +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/go/merge.go +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/hatch_build.py +0 -0
- {opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/src/opa_bindings/__init__.py +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: opa-golib-python-bindings
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python bindings for the OPA (Open Policy Agent) Rego engine via a Go c-shared library
|
|
5
|
+
Project-URL: Homepage, https://github.com/phi1010/opa-golib-python-bindings
|
|
6
|
+
Project-URL: Repository, https://github.com/phi1010/opa-golib-python-bindings
|
|
7
|
+
Project-URL: Issues, https://github.com/phi1010/opa-golib-python-bindings/issues
|
|
8
|
+
Author: Phillip Kuhrt
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: authorization,opa,open-policy-agent,policy,rego
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Go
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.14
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
24
|
+
Requires-Dist: pyyaml; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# opa-golib-python-bindings
|
|
28
|
+
|
|
29
|
+
Python bindings for the [OPA](https://www.openpolicyagent.org/) Rego engine, embedding
|
|
30
|
+
`github.com/open-policy-agent/opa/v1/rego` via a Go c-shared library and a stdlib-only
|
|
31
|
+
ctypes wrapper.
|
|
32
|
+
|
|
33
|
+
## Build
|
|
34
|
+
|
|
35
|
+
Requires Go >= 1.26 and Python >= 3.14.
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
make build # builds src/opa_bindings/libopabridge.so
|
|
39
|
+
make test # builds + runs pytest
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from opa_bindings import OpaEngine
|
|
46
|
+
|
|
47
|
+
users = {"alice": {"role": "admin"}}
|
|
48
|
+
|
|
49
|
+
with OpaEngine() as engine:
|
|
50
|
+
engine.add_policy("authz.rego", """
|
|
51
|
+
package authz
|
|
52
|
+
|
|
53
|
+
allow if lookup_user(input.user).role == "admin"
|
|
54
|
+
""")
|
|
55
|
+
engine.add_data({"admin": ["alice"]}, path="roles") # deep-merged; conflicts raise
|
|
56
|
+
engine.register_function("lookup_user", lambda name: users.get(name))
|
|
57
|
+
|
|
58
|
+
engine.eval_document("authz.allow", {"user": "alice"}) # -> True
|
|
59
|
+
engine.eval_query("x = data.roles.admin[_]") # -> [{"x": "alice"}]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Notes:
|
|
63
|
+
|
|
64
|
+
- `add_data` deep-merges objects; identical values coexist, conflicting values raise
|
|
65
|
+
`OpaError(code="merge_conflict")` naming the conflicting path.
|
|
66
|
+
- `register_function` infers arity from the callable's signature. A `*args` function is
|
|
67
|
+
variadic and is called from Rego with a single array argument: `many(["a", "b"])`
|
|
68
|
+
(OPA does not support variadic builtins with return values).
|
|
69
|
+
- Builtin arguments and return values are JSON-compatible objects. A callback exception
|
|
70
|
+
becomes an evaluation error; returning is fine.
|
|
71
|
+
- An undefined document raises `OpaUndefinedError`.
|
|
72
|
+
- Pass `coverage=True` to `eval_document` / `eval_query` to capture a coverage
|
|
73
|
+
report (OPA's `cover` tracer) in `engine.last_coverage`: per-file `covered` /
|
|
74
|
+
`not_covered` line ranges plus line counts and a coverage percentage over all
|
|
75
|
+
added policies. Evaluating without `coverage=True` resets it to `None`.
|
|
76
|
+
- Pass `trace=True` to capture the full evaluation trace in `engine.last_trace`:
|
|
77
|
+
a list of event dicts (`op`, `location`, `node`, `locals`, ...) in evaluation
|
|
78
|
+
order. `locals` holds the plugged variable bindings live at each step, so the
|
|
79
|
+
value a statement produced is visible (e.g. `{"x": 6}` after `x := input.n * 2`);
|
|
80
|
+
a false condition appears as a `Fail` event at its location. An undefined
|
|
81
|
+
document still carries its trace — the main way to see which condition failed. Note that `node`
|
|
82
|
+
shows the compiler-rewritten expression (temporaries like `__local0__`), a
|
|
83
|
+
statement may appear multiple times (`Redo` on backtracking), and tracing
|
|
84
|
+
slows evaluation, so keep it opt-in per call. Coverage only records *which*
|
|
85
|
+
statements were evaluated; traces are how to see their results.
|
|
86
|
+
- `compile_filters` partially evaluates a query and translates the residual
|
|
87
|
+
policy into a data filter (OPA's Compile-API / data-filter machinery):
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
engine.add_policy("filters.rego", """
|
|
91
|
+
package filters
|
|
92
|
+
|
|
93
|
+
include if input.fruits.colour == "green"
|
|
94
|
+
include if {
|
|
95
|
+
input.fruits.name == "banana"
|
|
96
|
+
input.user == "admin"
|
|
97
|
+
}
|
|
98
|
+
""")
|
|
99
|
+
engine.compile_filters(
|
|
100
|
+
"data.filters.include",
|
|
101
|
+
{"user": "admin"}, # known input
|
|
102
|
+
unknowns=["input.fruits"], # left symbolic
|
|
103
|
+
target="sql", dialect="postgresql",
|
|
104
|
+
)
|
|
105
|
+
# -> {"query": "WHERE (fruits.colour = E'green' OR fruits.name = E'banana')",
|
|
106
|
+
# "masks": None}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`target="sql"` (dialects `postgresql`, `mysql`, `sqlserver`, `sqlite`)
|
|
110
|
+
yields a WHERE clause string; `target="ucast"` (dialects `all`, `prisma`,
|
|
111
|
+
`linq`, or `""`) yields a UCAST condition dict (the `ucast.json` wire
|
|
112
|
+
format). `query` is `None` when the policy can never match and `""`/`{}`
|
|
113
|
+
when it always matches. `mappings` renames tables/columns (e.g.
|
|
114
|
+
`{"fruits": {"$self": "fruit_table", "colour": "col"}}`), and `mask_rule`
|
|
115
|
+
names a rule evaluated to produce column masks (returned under `"masks"`).
|
|
116
|
+
Residual conditions that cannot be expressed for the chosen target raise
|
|
117
|
+
`OpaError(code="compile_error")`.
|
|
118
|
+
|
|
119
|
+
Unknown refs must have the shape `input.<table>.<column>` — exactly two
|
|
120
|
+
segments after `input`, whatever the declared unknown boundary is, and for
|
|
121
|
+
every target/dialect (`ucast`/`all` included; `mappings` cannot deepen it).
|
|
122
|
+
So `unknowns=["input.item"]` permits only `input.item.<column>`, while the
|
|
123
|
+
bare `unknowns=["input"]` permits `input.<table>.<column>`. Deeper refs
|
|
124
|
+
like `input.item.attrs.price.value` fail with `pe_fragment_error: invalid
|
|
125
|
+
ref operand`, so nested documents (e.g. an EAV entity with per-attribute
|
|
126
|
+
type/value objects, or per-locale value objects) must be flattened into
|
|
127
|
+
columns (`input.attr.value_number`, `input.attr.value_de`, ...). Dynamic
|
|
128
|
+
column choice is fine as long as the key is known at compile time:
|
|
129
|
+
`input.attr[sprintf("value_%s", [input.locale])]` resolves to a single
|
|
130
|
+
column during partial evaluation.
|
|
131
|
+
- Rego `print(...)` output is captured per evaluation: set `engine.print_handler`
|
|
132
|
+
to a `callable(message, location)` to receive it (default: written to stderr);
|
|
133
|
+
`engine.last_prints` holds the `(message, location)` pairs of the last eval.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# opa-golib-python-bindings
|
|
2
|
+
|
|
3
|
+
Python bindings for the [OPA](https://www.openpolicyagent.org/) Rego engine, embedding
|
|
4
|
+
`github.com/open-policy-agent/opa/v1/rego` via a Go c-shared library and a stdlib-only
|
|
5
|
+
ctypes wrapper.
|
|
6
|
+
|
|
7
|
+
## Build
|
|
8
|
+
|
|
9
|
+
Requires Go >= 1.26 and Python >= 3.14.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
make build # builds src/opa_bindings/libopabridge.so
|
|
13
|
+
make test # builds + runs pytest
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from opa_bindings import OpaEngine
|
|
20
|
+
|
|
21
|
+
users = {"alice": {"role": "admin"}}
|
|
22
|
+
|
|
23
|
+
with OpaEngine() as engine:
|
|
24
|
+
engine.add_policy("authz.rego", """
|
|
25
|
+
package authz
|
|
26
|
+
|
|
27
|
+
allow if lookup_user(input.user).role == "admin"
|
|
28
|
+
""")
|
|
29
|
+
engine.add_data({"admin": ["alice"]}, path="roles") # deep-merged; conflicts raise
|
|
30
|
+
engine.register_function("lookup_user", lambda name: users.get(name))
|
|
31
|
+
|
|
32
|
+
engine.eval_document("authz.allow", {"user": "alice"}) # -> True
|
|
33
|
+
engine.eval_query("x = data.roles.admin[_]") # -> [{"x": "alice"}]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Notes:
|
|
37
|
+
|
|
38
|
+
- `add_data` deep-merges objects; identical values coexist, conflicting values raise
|
|
39
|
+
`OpaError(code="merge_conflict")` naming the conflicting path.
|
|
40
|
+
- `register_function` infers arity from the callable's signature. A `*args` function is
|
|
41
|
+
variadic and is called from Rego with a single array argument: `many(["a", "b"])`
|
|
42
|
+
(OPA does not support variadic builtins with return values).
|
|
43
|
+
- Builtin arguments and return values are JSON-compatible objects. A callback exception
|
|
44
|
+
becomes an evaluation error; returning is fine.
|
|
45
|
+
- An undefined document raises `OpaUndefinedError`.
|
|
46
|
+
- Pass `coverage=True` to `eval_document` / `eval_query` to capture a coverage
|
|
47
|
+
report (OPA's `cover` tracer) in `engine.last_coverage`: per-file `covered` /
|
|
48
|
+
`not_covered` line ranges plus line counts and a coverage percentage over all
|
|
49
|
+
added policies. Evaluating without `coverage=True` resets it to `None`.
|
|
50
|
+
- Pass `trace=True` to capture the full evaluation trace in `engine.last_trace`:
|
|
51
|
+
a list of event dicts (`op`, `location`, `node`, `locals`, ...) in evaluation
|
|
52
|
+
order. `locals` holds the plugged variable bindings live at each step, so the
|
|
53
|
+
value a statement produced is visible (e.g. `{"x": 6}` after `x := input.n * 2`);
|
|
54
|
+
a false condition appears as a `Fail` event at its location. An undefined
|
|
55
|
+
document still carries its trace — the main way to see which condition failed. Note that `node`
|
|
56
|
+
shows the compiler-rewritten expression (temporaries like `__local0__`), a
|
|
57
|
+
statement may appear multiple times (`Redo` on backtracking), and tracing
|
|
58
|
+
slows evaluation, so keep it opt-in per call. Coverage only records *which*
|
|
59
|
+
statements were evaluated; traces are how to see their results.
|
|
60
|
+
- `compile_filters` partially evaluates a query and translates the residual
|
|
61
|
+
policy into a data filter (OPA's Compile-API / data-filter machinery):
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
engine.add_policy("filters.rego", """
|
|
65
|
+
package filters
|
|
66
|
+
|
|
67
|
+
include if input.fruits.colour == "green"
|
|
68
|
+
include if {
|
|
69
|
+
input.fruits.name == "banana"
|
|
70
|
+
input.user == "admin"
|
|
71
|
+
}
|
|
72
|
+
""")
|
|
73
|
+
engine.compile_filters(
|
|
74
|
+
"data.filters.include",
|
|
75
|
+
{"user": "admin"}, # known input
|
|
76
|
+
unknowns=["input.fruits"], # left symbolic
|
|
77
|
+
target="sql", dialect="postgresql",
|
|
78
|
+
)
|
|
79
|
+
# -> {"query": "WHERE (fruits.colour = E'green' OR fruits.name = E'banana')",
|
|
80
|
+
# "masks": None}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`target="sql"` (dialects `postgresql`, `mysql`, `sqlserver`, `sqlite`)
|
|
84
|
+
yields a WHERE clause string; `target="ucast"` (dialects `all`, `prisma`,
|
|
85
|
+
`linq`, or `""`) yields a UCAST condition dict (the `ucast.json` wire
|
|
86
|
+
format). `query` is `None` when the policy can never match and `""`/`{}`
|
|
87
|
+
when it always matches. `mappings` renames tables/columns (e.g.
|
|
88
|
+
`{"fruits": {"$self": "fruit_table", "colour": "col"}}`), and `mask_rule`
|
|
89
|
+
names a rule evaluated to produce column masks (returned under `"masks"`).
|
|
90
|
+
Residual conditions that cannot be expressed for the chosen target raise
|
|
91
|
+
`OpaError(code="compile_error")`.
|
|
92
|
+
|
|
93
|
+
Unknown refs must have the shape `input.<table>.<column>` — exactly two
|
|
94
|
+
segments after `input`, whatever the declared unknown boundary is, and for
|
|
95
|
+
every target/dialect (`ucast`/`all` included; `mappings` cannot deepen it).
|
|
96
|
+
So `unknowns=["input.item"]` permits only `input.item.<column>`, while the
|
|
97
|
+
bare `unknowns=["input"]` permits `input.<table>.<column>`. Deeper refs
|
|
98
|
+
like `input.item.attrs.price.value` fail with `pe_fragment_error: invalid
|
|
99
|
+
ref operand`, so nested documents (e.g. an EAV entity with per-attribute
|
|
100
|
+
type/value objects, or per-locale value objects) must be flattened into
|
|
101
|
+
columns (`input.attr.value_number`, `input.attr.value_de`, ...). Dynamic
|
|
102
|
+
column choice is fine as long as the key is known at compile time:
|
|
103
|
+
`input.attr[sprintf("value_%s", [input.locale])]` resolves to a single
|
|
104
|
+
column during partial evaluation.
|
|
105
|
+
- Rego `print(...)` output is captured per evaluation: set `engine.print_handler`
|
|
106
|
+
to a `callable(message, location)` to receive it (default: written to stderr);
|
|
107
|
+
`engine.last_prints` holds the `(message, location)` pairs of the last eval.
|
|
@@ -21,6 +21,7 @@ import (
|
|
|
21
21
|
"unsafe"
|
|
22
22
|
|
|
23
23
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
24
|
+
"github.com/open-policy-agent/opa/v1/cover"
|
|
24
25
|
"github.com/open-policy-agent/opa/v1/rego"
|
|
25
26
|
"github.com/open-policy-agent/opa/v1/storage/inmem"
|
|
26
27
|
"github.com/open-policy-agent/opa/v1/topdown/print"
|
|
@@ -187,21 +188,21 @@ func OpaRegisterBuiltin(h C.ulonglong, name *C.char, arity C.int, cb C.opa_callb
|
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
//export OpaEvalQuery
|
|
190
|
-
func OpaEvalQuery(h C.ulonglong, query, inputJson *C.char) *C.char {
|
|
191
|
-
return evalCommon(h, C.GoString(query), inputJson)
|
|
191
|
+
func OpaEvalQuery(h C.ulonglong, query, inputJson *C.char, coverage, trace C.int) *C.char {
|
|
192
|
+
return evalCommon(h, C.GoString(query), inputJson, coverage != 0, trace != 0)
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
//export OpaEvalDocument
|
|
195
|
-
func OpaEvalDocument(h C.ulonglong, docPath, inputJson *C.char) *C.char {
|
|
196
|
+
func OpaEvalDocument(h C.ulonglong, docPath, inputJson *C.char, coverage, trace C.int) *C.char {
|
|
196
197
|
p := C.GoString(docPath)
|
|
197
198
|
q := "data"
|
|
198
199
|
if p != "" {
|
|
199
200
|
q = "data." + p
|
|
200
201
|
}
|
|
201
|
-
return evalCommon(h, q, inputJson)
|
|
202
|
+
return evalCommon(h, q, inputJson, coverage != 0, trace != 0)
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
func evalCommon(h C.ulonglong, query string, inputJson *C.char) *C.char {
|
|
205
|
+
func evalCommon(h C.ulonglong, query string, inputJson *C.char, coverage, trace bool) *C.char {
|
|
205
206
|
e, err := getEngine(h)
|
|
206
207
|
if err != nil {
|
|
207
208
|
return errorJSON("invalid_handle", err.Error())
|
|
@@ -243,11 +244,36 @@ func evalCommon(h C.ulonglong, query string, inputJson *C.char) *C.char {
|
|
|
243
244
|
}
|
|
244
245
|
collector := &printCollector{}
|
|
245
246
|
evalOpts = append(evalOpts, rego.EvalPrintHook(collector))
|
|
247
|
+
var cov *cover.Cover
|
|
248
|
+
if coverage {
|
|
249
|
+
cov = cover.New()
|
|
250
|
+
evalOpts = append(evalOpts, rego.EvalQueryTracer(cov))
|
|
251
|
+
}
|
|
252
|
+
var tracer *traceCollector
|
|
253
|
+
if trace {
|
|
254
|
+
tracer = &traceCollector{}
|
|
255
|
+
evalOpts = append(evalOpts, rego.EvalQueryTracer(tracer))
|
|
256
|
+
}
|
|
246
257
|
rs, err := pq.Eval(context.Background(), evalOpts...)
|
|
247
258
|
if err != nil {
|
|
248
259
|
return errorJSON("eval_error", err.Error())
|
|
249
260
|
}
|
|
250
|
-
|
|
261
|
+
envelope := map[string]any{"result": rs, "prints": collector.msgs}
|
|
262
|
+
if cov != nil {
|
|
263
|
+
e.mu.Lock()
|
|
264
|
+
parsed := map[string]*ast.Module{}
|
|
265
|
+
for path, src := range e.modules {
|
|
266
|
+
if m, perr := ast.ParseModule(path, src); perr == nil {
|
|
267
|
+
parsed[path] = m
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
e.mu.Unlock()
|
|
271
|
+
envelope["coverage"] = cov.Report(parsed)
|
|
272
|
+
}
|
|
273
|
+
if tracer != nil {
|
|
274
|
+
envelope["trace"] = tracer.events
|
|
275
|
+
}
|
|
276
|
+
b, err := json.Marshal(envelope)
|
|
251
277
|
if err != nil {
|
|
252
278
|
return errorJSON("internal", err.Error())
|
|
253
279
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
#include <stdlib.h>
|
|
5
|
+
*/
|
|
6
|
+
import "C"
|
|
7
|
+
|
|
8
|
+
import (
|
|
9
|
+
"context"
|
|
10
|
+
"encoding/json"
|
|
11
|
+
|
|
12
|
+
"github.com/open-policy-agent/opa/v1/ast"
|
|
13
|
+
"github.com/open-policy-agent/opa/v1/rego"
|
|
14
|
+
regocompile "github.com/open-policy-agent/opa/v1/rego/compile"
|
|
15
|
+
"github.com/open-policy-agent/opa/v1/storage/inmem"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// OpaCompileFilters partially evaluates a query with respect to the given
|
|
19
|
+
// unknowns and translates the residual into a data filter for the given
|
|
20
|
+
// target/dialect ("sql" with postgresql/mysql/sqlserver/sqlite, or "ucast"
|
|
21
|
+
// with all/prisma/linq/""). The result envelope holds
|
|
22
|
+
// {"query": <string or object>, "masks": <object or null>}.
|
|
23
|
+
//
|
|
24
|
+
//export OpaCompileFilters
|
|
25
|
+
func OpaCompileFilters(h C.ulonglong, query, inputJson, unknownsJson, target, dialect, mappingsJson, maskRule *C.char) *C.char {
|
|
26
|
+
e, err := getEngine(h)
|
|
27
|
+
if err != nil {
|
|
28
|
+
return errorJSON("invalid_handle", err.Error())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
parsedQuery, err := ast.ParseBody(C.GoString(query))
|
|
32
|
+
if err != nil {
|
|
33
|
+
return errorJSON("parse_error", err.Error())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var unknownStrs []string
|
|
37
|
+
if err := json.Unmarshal([]byte(C.GoString(unknownsJson)), &unknownStrs); err != nil {
|
|
38
|
+
return errorJSON("invalid_json", err.Error())
|
|
39
|
+
}
|
|
40
|
+
unknowns := make([]*ast.Term, len(unknownStrs))
|
|
41
|
+
for i, u := range unknownStrs {
|
|
42
|
+
if unknowns[i], err = ast.ParseTerm(u); err != nil {
|
|
43
|
+
return errorJSON("parse_error", "unknown "+u+": "+err.Error())
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
tgt, dia := C.GoString(target), C.GoString(dialect)
|
|
48
|
+
copts := []regocompile.CompileOption{
|
|
49
|
+
regocompile.ParsedQuery(parsedQuery),
|
|
50
|
+
regocompile.ParsedUnknowns(unknowns...),
|
|
51
|
+
regocompile.Target(tgt, dia),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if s := C.GoString(mappingsJson); s != "" {
|
|
55
|
+
var mappings map[string]any
|
|
56
|
+
if err := json.Unmarshal([]byte(s), &mappings); err != nil {
|
|
57
|
+
return errorJSON("invalid_json", err.Error())
|
|
58
|
+
}
|
|
59
|
+
copts = append(copts, regocompile.Mappings(mappings))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if s := C.GoString(maskRule); s != "" {
|
|
63
|
+
ref, err := ast.ParseRef(s)
|
|
64
|
+
if err != nil {
|
|
65
|
+
return errorJSON("parse_error", "mask rule: "+err.Error())
|
|
66
|
+
}
|
|
67
|
+
copts = append(copts, regocompile.MaskRule(ref))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
e.mu.Lock()
|
|
71
|
+
ropts := []func(*rego.Rego){
|
|
72
|
+
rego.Store(inmem.NewFromObject(e.data)),
|
|
73
|
+
rego.StrictBuiltinErrors(true),
|
|
74
|
+
}
|
|
75
|
+
for path, src := range e.modules {
|
|
76
|
+
ropts = append(ropts, rego.Module(path, src))
|
|
77
|
+
}
|
|
78
|
+
for _, b := range e.builtins {
|
|
79
|
+
ropts = append(ropts, makeBuiltin(uint64(h), b))
|
|
80
|
+
}
|
|
81
|
+
e.mu.Unlock()
|
|
82
|
+
copts = append(copts, regocompile.Rego(ropts...))
|
|
83
|
+
|
|
84
|
+
prepared, err := regocompile.New(copts...).Prepare(context.Background())
|
|
85
|
+
if err != nil {
|
|
86
|
+
return errorJSON("prepare_error", err.Error())
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
evalOpts := []rego.EvalOption{}
|
|
90
|
+
if inputJson != nil {
|
|
91
|
+
if s := C.GoString(inputJson); s != "" {
|
|
92
|
+
var input any
|
|
93
|
+
if err := json.Unmarshal([]byte(s), &input); err != nil {
|
|
94
|
+
return errorJSON("invalid_json", err.Error())
|
|
95
|
+
}
|
|
96
|
+
evalOpts = append(evalOpts, rego.EvalInput(input))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
filters, err := prepared.Compile(context.Background(), evalOpts...)
|
|
101
|
+
if err != nil {
|
|
102
|
+
return errorJSON("compile_error", err.Error())
|
|
103
|
+
}
|
|
104
|
+
f := filters.For(tgt, dia)
|
|
105
|
+
return resultJSON(map[string]any{"query": f.Query, "masks": f.Masks})
|
|
106
|
+
}
|
|
@@ -11,6 +11,9 @@ require (
|
|
|
11
11
|
github.com/gobwas/glob v0.2.3 // indirect
|
|
12
12
|
github.com/goccy/go-json v0.10.6 // indirect
|
|
13
13
|
github.com/google/uuid v1.6.0 // indirect
|
|
14
|
+
github.com/huandu/go-clone v1.7.3 // indirect
|
|
15
|
+
github.com/huandu/go-sqlbuilder v1.42.1 // indirect
|
|
16
|
+
github.com/huandu/xstrings v1.4.0 // indirect
|
|
14
17
|
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
|
15
18
|
github.com/lestrrat-go/dsig v1.2.1 // indirect
|
|
16
19
|
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
|
|
@@ -37,6 +37,15 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
|
37
37
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
38
38
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
39
39
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
40
|
+
github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=
|
|
41
|
+
github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs=
|
|
42
|
+
github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs=
|
|
43
|
+
github.com/huandu/go-clone v1.7.3 h1:rtQODA+ABThEn6J5LBTppJfKmZy/FwfpMUWa8d01TTQ=
|
|
44
|
+
github.com/huandu/go-clone v1.7.3/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE=
|
|
45
|
+
github.com/huandu/go-sqlbuilder v1.42.1 h1:9DVQbKg1uFNGaVqgWMpaBNkM3Fv58p2m/+AxWkTrCYc=
|
|
46
|
+
github.com/huandu/go-sqlbuilder v1.42.1/go.mod h1:BEm32AHl29lzKDeV3HAIkzrz9cgRyumkDohHeGYYBoM=
|
|
47
|
+
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
|
48
|
+
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
|
40
49
|
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
|
|
41
50
|
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
|
42
51
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
@@ -82,6 +91,7 @@ github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr
|
|
|
82
91
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
|
83
92
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
|
84
93
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
94
|
+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
85
95
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
86
96
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
87
97
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
@@ -128,6 +138,7 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
|
|
|
128
138
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
129
139
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
130
140
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
141
|
+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
131
142
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
132
143
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
133
144
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"strings"
|
|
6
|
+
"sync"
|
|
7
|
+
|
|
8
|
+
"github.com/open-policy-agent/opa/v1/ast"
|
|
9
|
+
"github.com/open-policy-agent/opa/v1/topdown"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
type traceEvent struct {
|
|
13
|
+
Op string `json:"op"`
|
|
14
|
+
QueryID uint64 `json:"query_id"`
|
|
15
|
+
ParentID uint64 `json:"parent_id"`
|
|
16
|
+
Location string `json:"location,omitempty"`
|
|
17
|
+
Node string `json:"node,omitempty"`
|
|
18
|
+
Locals map[string]any `json:"locals,omitempty"`
|
|
19
|
+
Message string `json:"message,omitempty"`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// traceCollector implements topdown.QueryTracer, capturing every evaluation
|
|
23
|
+
// event together with the local variable bindings live at that point.
|
|
24
|
+
type traceCollector struct {
|
|
25
|
+
mu sync.Mutex
|
|
26
|
+
events []traceEvent
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func (t *traceCollector) Enabled() bool { return true }
|
|
30
|
+
|
|
31
|
+
func (t *traceCollector) Config() topdown.TraceConfig {
|
|
32
|
+
return topdown.TraceConfig{PlugLocalVars: true}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func (t *traceCollector) TraceEvent(evt topdown.Event) {
|
|
36
|
+
te := traceEvent{
|
|
37
|
+
Op: string(evt.Op),
|
|
38
|
+
QueryID: evt.QueryID,
|
|
39
|
+
ParentID: evt.ParentID,
|
|
40
|
+
Message: evt.Message,
|
|
41
|
+
}
|
|
42
|
+
if evt.Location != nil {
|
|
43
|
+
te.Location = evt.Location.String()
|
|
44
|
+
}
|
|
45
|
+
if evt.Node != nil {
|
|
46
|
+
te.Node = fmt.Sprintf("%v", evt.Node)
|
|
47
|
+
}
|
|
48
|
+
if evt.Locals != nil {
|
|
49
|
+
locals := map[string]any{}
|
|
50
|
+
evt.Locals.Iter(func(k, v ast.Value) bool {
|
|
51
|
+
kv, ok := k.(ast.Var)
|
|
52
|
+
if !ok {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
name := string(kv)
|
|
56
|
+
// Compiler-generated vars carry the user-facing name in the
|
|
57
|
+
// event metadata; drop them if no such name exists.
|
|
58
|
+
if md, found := evt.LocalMetadata[kv]; found {
|
|
59
|
+
name = string(md.Name)
|
|
60
|
+
}
|
|
61
|
+
if strings.HasPrefix(name, "__local") || strings.HasPrefix(name, "$") {
|
|
62
|
+
return false // compiler temporaries and wildcards
|
|
63
|
+
}
|
|
64
|
+
if j, err := ast.JSON(v); err == nil {
|
|
65
|
+
locals[name] = j
|
|
66
|
+
} else {
|
|
67
|
+
locals[name] = v.String()
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
})
|
|
71
|
+
if len(locals) > 0 {
|
|
72
|
+
te.Locals = locals
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
t.mu.Lock()
|
|
76
|
+
t.events = append(t.events, te)
|
|
77
|
+
t.mu.Unlock()
|
|
78
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "opa-golib-python-bindings"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Python bindings for the OPA (Open Policy Agent) Rego engine via a Go c-shared library"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -26,7 +26,7 @@ Repository = "https://github.com/phi1010/opa-golib-python-bindings"
|
|
|
26
26
|
Issues = "https://github.com/phi1010/opa-golib-python-bindings/issues"
|
|
27
27
|
|
|
28
28
|
[project.optional-dependencies]
|
|
29
|
-
dev = ["pytest"]
|
|
29
|
+
dev = ["pytest", "pyyaml"]
|
|
30
30
|
|
|
31
31
|
[build-system]
|
|
32
32
|
requires = ["hatchling"]
|
{opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/src/opa_bindings/_native.py
RENAMED
|
@@ -43,9 +43,33 @@ def load():
|
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
lib.OpaEvalQuery.restype = ctypes.c_void_p
|
|
46
|
-
lib.OpaEvalQuery.argtypes = [
|
|
46
|
+
lib.OpaEvalQuery.argtypes = [
|
|
47
|
+
ctypes.c_uint64,
|
|
48
|
+
ctypes.c_char_p,
|
|
49
|
+
ctypes.c_char_p,
|
|
50
|
+
ctypes.c_int, # coverage
|
|
51
|
+
ctypes.c_int, # trace
|
|
52
|
+
]
|
|
47
53
|
|
|
48
54
|
lib.OpaEvalDocument.restype = ctypes.c_void_p
|
|
49
|
-
lib.OpaEvalDocument.argtypes = [
|
|
55
|
+
lib.OpaEvalDocument.argtypes = [
|
|
56
|
+
ctypes.c_uint64,
|
|
57
|
+
ctypes.c_char_p,
|
|
58
|
+
ctypes.c_char_p,
|
|
59
|
+
ctypes.c_int, # coverage
|
|
60
|
+
ctypes.c_int, # trace
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
lib.OpaCompileFilters.restype = ctypes.c_void_p
|
|
64
|
+
lib.OpaCompileFilters.argtypes = [
|
|
65
|
+
ctypes.c_uint64,
|
|
66
|
+
ctypes.c_char_p, # query
|
|
67
|
+
ctypes.c_char_p, # input JSON
|
|
68
|
+
ctypes.c_char_p, # unknowns JSON array
|
|
69
|
+
ctypes.c_char_p, # target
|
|
70
|
+
ctypes.c_char_p, # dialect
|
|
71
|
+
ctypes.c_char_p, # mappings JSON
|
|
72
|
+
ctypes.c_char_p, # mask rule ref
|
|
73
|
+
]
|
|
50
74
|
|
|
51
75
|
return lib
|
{opa_golib_python_bindings-0.1.2 → opa_golib_python_bindings-0.3.0}/src/opa_bindings/engine.py
RENAMED
|
@@ -50,6 +50,17 @@ class OpaEngine:
|
|
|
50
50
|
self.print_handler = None
|
|
51
51
|
#: Prints captured by the most recent eval, as (message, location).
|
|
52
52
|
self.last_prints = []
|
|
53
|
+
#: Coverage report of the most recent eval with ``coverage=True``:
|
|
54
|
+
#: {"files": {path: {"covered": [...], "not_covered": [...], ...}},
|
|
55
|
+
#: "covered_lines": int, "not_covered_lines": int, "coverage": float}.
|
|
56
|
+
#: None if the last eval did not capture coverage.
|
|
57
|
+
self.last_coverage = None
|
|
58
|
+
#: Evaluation trace of the most recent eval with ``trace=True``: a
|
|
59
|
+
#: list of event dicts {"op", "query_id", "parent_id", "location",
|
|
60
|
+
#: "node", "locals", "message"} in evaluation order, where "locals"
|
|
61
|
+
#: holds the variable bindings live at that point. None if the last
|
|
62
|
+
#: eval did not capture a trace.
|
|
63
|
+
self.last_trace = None
|
|
53
64
|
|
|
54
65
|
# -- lifecycle -----------------------------------------------------
|
|
55
66
|
|
|
@@ -90,7 +101,16 @@ class OpaEngine:
|
|
|
90
101
|
raise OpaError(err.get("code", "unknown"), err.get("message", ""))
|
|
91
102
|
return envelope
|
|
92
103
|
|
|
104
|
+
def _eval_reset(self):
|
|
105
|
+
# Clear per-eval state up front so a failed eval never leaves stale
|
|
106
|
+
# results from a previous evaluation behind.
|
|
107
|
+
self.last_coverage = None
|
|
108
|
+
self.last_trace = None
|
|
109
|
+
self.last_prints = []
|
|
110
|
+
|
|
93
111
|
def _eval_result(self, envelope):
|
|
112
|
+
self.last_coverage = envelope.get("coverage")
|
|
113
|
+
self.last_trace = envelope.get("trace")
|
|
94
114
|
self.last_prints = [
|
|
95
115
|
(p["message"], p["location"]) for p in envelope.get("prints") or []
|
|
96
116
|
]
|
|
@@ -166,29 +186,111 @@ class OpaEngine:
|
|
|
166
186
|
del self._functions[name]
|
|
167
187
|
raise
|
|
168
188
|
|
|
169
|
-
def eval_query(
|
|
170
|
-
|
|
189
|
+
def eval_query(
|
|
190
|
+
self, query: str, input=None, *, coverage: bool = False, trace: bool = False
|
|
191
|
+
):
|
|
192
|
+
"""Evaluate a Rego query; returns a list of binding dicts.
|
|
193
|
+
|
|
194
|
+
With ``coverage=True`` the evaluation is traced and a coverage report
|
|
195
|
+
over all added policies is stored in ``self.last_coverage``. With
|
|
196
|
+
``trace=True`` the full event trace, including the variable bindings
|
|
197
|
+
at each step, is stored in ``self.last_trace``.
|
|
198
|
+
"""
|
|
171
199
|
self._check_open()
|
|
200
|
+
self._eval_reset()
|
|
172
201
|
rs = self._eval_result(
|
|
173
|
-
self._call(
|
|
202
|
+
self._call(
|
|
203
|
+
self._lib.OpaEvalQuery,
|
|
204
|
+
query.encode(),
|
|
205
|
+
_encode_input(input),
|
|
206
|
+
int(coverage),
|
|
207
|
+
int(trace),
|
|
208
|
+
)
|
|
174
209
|
)
|
|
175
210
|
if not rs:
|
|
176
211
|
return []
|
|
177
212
|
return [r.get("bindings", {}) for r in rs]
|
|
178
213
|
|
|
179
|
-
def eval_document(
|
|
214
|
+
def eval_document(
|
|
215
|
+
self, path: str, input=None, *, coverage: bool = False, trace: bool = False
|
|
216
|
+
):
|
|
180
217
|
"""Evaluate the document at ``data.<path>`` and return its value.
|
|
181
218
|
|
|
182
|
-
Raises OpaUndefinedError if the document is undefined.
|
|
219
|
+
Raises OpaUndefinedError if the document is undefined. With
|
|
220
|
+
``coverage=True`` a coverage report is stored in ``self.last_coverage``;
|
|
221
|
+
with ``trace=True`` the event trace is stored in ``self.last_trace``.
|
|
183
222
|
"""
|
|
184
223
|
self._check_open()
|
|
224
|
+
self._eval_reset()
|
|
185
225
|
rs = self._eval_result(
|
|
186
|
-
self._call(
|
|
226
|
+
self._call(
|
|
227
|
+
self._lib.OpaEvalDocument,
|
|
228
|
+
path.encode(),
|
|
229
|
+
_encode_input(input),
|
|
230
|
+
int(coverage),
|
|
231
|
+
int(trace),
|
|
232
|
+
)
|
|
187
233
|
)
|
|
188
234
|
if not rs or not rs[0].get("expressions"):
|
|
189
235
|
raise OpaUndefinedError(f"data.{path}" if path else "data")
|
|
190
236
|
return rs[0]["expressions"][0]["value"]
|
|
191
237
|
|
|
238
|
+
def compile_filters(
|
|
239
|
+
self,
|
|
240
|
+
query: str,
|
|
241
|
+
input=None,
|
|
242
|
+
*,
|
|
243
|
+
unknowns=("input",),
|
|
244
|
+
target: str = "sql",
|
|
245
|
+
dialect: str = "postgresql",
|
|
246
|
+
mappings=None,
|
|
247
|
+
mask_rule: str | None = None,
|
|
248
|
+
):
|
|
249
|
+
"""Partially evaluate ``query`` and translate it into a data filter.
|
|
250
|
+
|
|
251
|
+
Everything under the refs in ``unknowns`` (e.g. ``"input.fruits"``)
|
|
252
|
+
is left unknown; the rest is evaluated using ``input`` and the data
|
|
253
|
+
and policies already added. The residual conditions are translated
|
|
254
|
+
for ``target``/``dialect``:
|
|
255
|
+
|
|
256
|
+
- ``target="sql"`` with dialect ``postgresql``, ``mysql``,
|
|
257
|
+
``sqlserver`` or ``sqlite``: returns a SQL WHERE clause string.
|
|
258
|
+
- ``target="ucast"`` with dialect ``all``, ``prisma``, ``linq`` or
|
|
259
|
+
``""``: returns a UCAST condition object (JSON-compatible dict).
|
|
260
|
+
|
|
261
|
+
``mappings`` optionally renames tables/columns (see the OPA docs on
|
|
262
|
+
Compile API mappings); ``mask_rule`` names a rule (e.g.
|
|
263
|
+
``"data.filters.masks"``) evaluated to produce column masks.
|
|
264
|
+
|
|
265
|
+
Returns ``{"query": <str or dict or None>, "masks": <dict or None>}``.
|
|
266
|
+
A query of ``None`` means the policy can never be satisfied; an empty
|
|
267
|
+
query means it is always satisfied. Raises OpaError with code
|
|
268
|
+
``compile_error`` if the residual policy cannot be expressed as a
|
|
269
|
+
filter for the chosen target.
|
|
270
|
+
|
|
271
|
+
Unknown refs are limited to ``input.<table>.<column>`` — exactly two
|
|
272
|
+
segments after ``input``, for every target/dialect, wherever the
|
|
273
|
+
declared unknown boundary sits (``mappings`` cannot deepen this):
|
|
274
|
+
``unknowns=["input.item"]`` permits only ``input.item.<column>``,
|
|
275
|
+
the bare ``unknowns=["input"]`` permits ``input.<table>.<column>``.
|
|
276
|
+
Nested documents like ``input.item.attrs.price.value`` are rejected
|
|
277
|
+
and must be flattened into columns; a dynamic column picked from
|
|
278
|
+
known values (``input.attr[sprintf("value_%s", [input.locale])]``)
|
|
279
|
+
is fine, as it resolves during partial evaluation.
|
|
280
|
+
"""
|
|
281
|
+
self._check_open()
|
|
282
|
+
envelope = self._call(
|
|
283
|
+
self._lib.OpaCompileFilters,
|
|
284
|
+
query.encode(),
|
|
285
|
+
_encode_input(input),
|
|
286
|
+
json.dumps(list(unknowns)).encode(),
|
|
287
|
+
target.encode(),
|
|
288
|
+
dialect.encode(),
|
|
289
|
+
b"" if mappings is None else json.dumps(mappings).encode(),
|
|
290
|
+
(mask_rule or "").encode(),
|
|
291
|
+
)
|
|
292
|
+
return envelope.get("result")
|
|
293
|
+
|
|
192
294
|
|
|
193
295
|
def _encode_input(input):
|
|
194
296
|
if input is None:
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: opa-golib-python-bindings
|
|
3
|
-
Version: 0.1.2
|
|
4
|
-
Summary: Python bindings for the OPA (Open Policy Agent) Rego engine via a Go c-shared library
|
|
5
|
-
Project-URL: Homepage, https://github.com/phi1010/opa-golib-python-bindings
|
|
6
|
-
Project-URL: Repository, https://github.com/phi1010/opa-golib-python-bindings
|
|
7
|
-
Project-URL: Issues, https://github.com/phi1010/opa-golib-python-bindings/issues
|
|
8
|
-
Author: Phillip Kuhrt
|
|
9
|
-
License-Expression: Apache-2.0
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: authorization,opa,open-policy-agent,policy,rego
|
|
12
|
-
Classifier: Development Status :: 3 - Alpha
|
|
13
|
-
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: Operating System :: MacOS
|
|
15
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
-
Classifier: Programming Language :: Go
|
|
17
|
-
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
-
Classifier: Topic :: Security
|
|
20
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
-
Requires-Python: >=3.14
|
|
22
|
-
Provides-Extra: dev
|
|
23
|
-
Requires-Dist: pytest; extra == 'dev'
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
|
|
26
|
-
# opa-golib-python-bindings
|
|
27
|
-
|
|
28
|
-
Python bindings for the [OPA](https://www.openpolicyagent.org/) Rego engine, embedding
|
|
29
|
-
`github.com/open-policy-agent/opa/v1/rego` via a Go c-shared library and a stdlib-only
|
|
30
|
-
ctypes wrapper.
|
|
31
|
-
|
|
32
|
-
## Build
|
|
33
|
-
|
|
34
|
-
Requires Go >= 1.26 and Python >= 3.14.
|
|
35
|
-
|
|
36
|
-
```sh
|
|
37
|
-
make build # builds src/opa_bindings/libopabridge.so
|
|
38
|
-
make test # builds + runs pytest
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Usage
|
|
42
|
-
|
|
43
|
-
```python
|
|
44
|
-
from opa_bindings import OpaEngine
|
|
45
|
-
|
|
46
|
-
users = {"alice": {"role": "admin"}}
|
|
47
|
-
|
|
48
|
-
with OpaEngine() as engine:
|
|
49
|
-
engine.add_policy("authz.rego", """
|
|
50
|
-
package authz
|
|
51
|
-
|
|
52
|
-
allow if lookup_user(input.user).role == "admin"
|
|
53
|
-
""")
|
|
54
|
-
engine.add_data({"admin": ["alice"]}, path="roles") # deep-merged; conflicts raise
|
|
55
|
-
engine.register_function("lookup_user", lambda name: users.get(name))
|
|
56
|
-
|
|
57
|
-
engine.eval_document("authz.allow", {"user": "alice"}) # -> True
|
|
58
|
-
engine.eval_query("x = data.roles.admin[_]") # -> [{"x": "alice"}]
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Notes:
|
|
62
|
-
|
|
63
|
-
- `add_data` deep-merges objects; identical values coexist, conflicting values raise
|
|
64
|
-
`OpaError(code="merge_conflict")` naming the conflicting path.
|
|
65
|
-
- `register_function` infers arity from the callable's signature. A `*args` function is
|
|
66
|
-
variadic and is called from Rego with a single array argument: `many(["a", "b"])`
|
|
67
|
-
(OPA does not support variadic builtins with return values).
|
|
68
|
-
- Builtin arguments and return values are JSON-compatible objects. A callback exception
|
|
69
|
-
becomes an evaluation error; returning is fine.
|
|
70
|
-
- An undefined document raises `OpaUndefinedError`.
|
|
71
|
-
- Rego `print(...)` output is captured per evaluation: set `engine.print_handler`
|
|
72
|
-
to a `callable(message, location)` to receive it (default: written to stderr);
|
|
73
|
-
`engine.last_prints` holds the `(message, location)` pairs of the last eval.
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# opa-golib-python-bindings
|
|
2
|
-
|
|
3
|
-
Python bindings for the [OPA](https://www.openpolicyagent.org/) Rego engine, embedding
|
|
4
|
-
`github.com/open-policy-agent/opa/v1/rego` via a Go c-shared library and a stdlib-only
|
|
5
|
-
ctypes wrapper.
|
|
6
|
-
|
|
7
|
-
## Build
|
|
8
|
-
|
|
9
|
-
Requires Go >= 1.26 and Python >= 3.14.
|
|
10
|
-
|
|
11
|
-
```sh
|
|
12
|
-
make build # builds src/opa_bindings/libopabridge.so
|
|
13
|
-
make test # builds + runs pytest
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Usage
|
|
17
|
-
|
|
18
|
-
```python
|
|
19
|
-
from opa_bindings import OpaEngine
|
|
20
|
-
|
|
21
|
-
users = {"alice": {"role": "admin"}}
|
|
22
|
-
|
|
23
|
-
with OpaEngine() as engine:
|
|
24
|
-
engine.add_policy("authz.rego", """
|
|
25
|
-
package authz
|
|
26
|
-
|
|
27
|
-
allow if lookup_user(input.user).role == "admin"
|
|
28
|
-
""")
|
|
29
|
-
engine.add_data({"admin": ["alice"]}, path="roles") # deep-merged; conflicts raise
|
|
30
|
-
engine.register_function("lookup_user", lambda name: users.get(name))
|
|
31
|
-
|
|
32
|
-
engine.eval_document("authz.allow", {"user": "alice"}) # -> True
|
|
33
|
-
engine.eval_query("x = data.roles.admin[_]") # -> [{"x": "alice"}]
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Notes:
|
|
37
|
-
|
|
38
|
-
- `add_data` deep-merges objects; identical values coexist, conflicting values raise
|
|
39
|
-
`OpaError(code="merge_conflict")` naming the conflicting path.
|
|
40
|
-
- `register_function` infers arity from the callable's signature. A `*args` function is
|
|
41
|
-
variadic and is called from Rego with a single array argument: `many(["a", "b"])`
|
|
42
|
-
(OPA does not support variadic builtins with return values).
|
|
43
|
-
- Builtin arguments and return values are JSON-compatible objects. A callback exception
|
|
44
|
-
becomes an evaluation error; returning is fine.
|
|
45
|
-
- An undefined document raises `OpaUndefinedError`.
|
|
46
|
-
- Rego `print(...)` output is captured per evaluation: set `engine.print_handler`
|
|
47
|
-
to a `callable(message, location)` to receive it (default: written to stderr);
|
|
48
|
-
`engine.last_prints` holds the `(message, location)` pairs of the last eval.
|
|
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.1.2 → opa_golib_python_bindings-0.3.0}/src/opa_bindings/__init__.py
RENAMED
|
File without changes
|