Writing a policy
The policy receives what a statement does and returns allow. It is written
in Rego and
evaluated by an OPA engine embedded in curral, so there is no sidecar.
A first policy
Section titled “A first policy”package curralimport rego.v1
default allow := false
allow if "admin" in input.roles
allow if { "analyst" in input.roles input.resolved input.statement_type == "SELECT" every t in input.tables { startswith(t, "sales.") }}curral serve ... --policy policy.rego- Always start from
default allow := false. - Require
input.resolvedfor anyone who is not an admin. It isfalsewhenever the inspection could not fully determine what a statement reads or writes. - Table names are fully qualified:
catalog.schema.table.
Every field the policy receives is listed in Policy input.
Keep data out of the rules
Section titled “Keep data out of the rules”--policy is repeatable and accepts JSON or YAML as well as .rego. Data
files are available as data.*, so grants can live in a file that is
easier to edit than the rules:
{ "allowed_functions": ["range", "generate_series", "unnest"], "roles": { "analyst": { "read": ["sales", "logs"], "write": [], "deny_tables": ["sales.main.salaries"] }, "etl": { "read": ["logs"], "write": ["sales"], "deny_tables": [] } }}curral serve ... --policy policy.rego --policy roles.jsonexamples/policy.rego
is a complete role-based model over that data: admin runs anything,
readers run SELECT/EXPLAIN on readable databases, writers run
INSERT/UPDATE/DELETE on writable ones, and DDL stays with admins.
Table functions
Section titled “Table functions”Functions such as read_parquet or read_csv read data without going
through a table, so they bypass table grants. They arrive in
input.functions; allow them explicitly:
every f in input.functions { f in data.allowed_functions }Debug with dry_run
Section titled “Debug with dry_run”dry_run runs inspection and policy without executing, and returns what the
policy saw and decided:
curl -u analyst:analyst-pw localhost:8080/v1/query \ -d '{"sql":"DELETE FROM orders WHERE id = 1","dry_run":true}'{"dry_run":true,"decision":"deny","decided_by":"policy","statement_type":"DELETE", "database":"sales","tables":["sales.main.orders"],"targets":["sales.main.orders"], "functions":[],"databases":["sales"],"resolved":true,"policy_sha256":"..."}decided_by: engine means the statement is one curral always denies, such
as ATTACH or INSTALL.
Optional rules
Section titled “Optional rules”The same policy can return more than allow. Each rule is turned on by a
flag:
| Rule | Flag | |
|---|---|---|
data.curral.limits |
--policy-limits-query |
timeout, row limit and concurrency per request; see Limits |
data.curral.masks |
--policy-masks-query |
column masks; see Column masking |
Row filters live in their own file; see Row-level security.
Changing the policy
Section titled “Changing the policy”Edit the files and send SIGHUP. An invalid policy is rejected and the
previous one stays in effect. The hash of the policy in effect is in every
audit event (policy_sha256). See
Reloading configuration.