Write a Template
A template is a Rego module that defines a kind of rule. When the two templates Kamiwaza ships (Restrict a Data Source) don't express the rule you need, write your own.
This page covers what Kamiwaza expects of a template. For the Rego language
itself, see OPA's policy language
and policy reference,
and test templates locally with opa test.
| Step | What |
|---|---|
| 1 | Write the template and its parameter schema |
| 2 | Start a draft, and wait for your OPA to compile it |
| 3 | Try it on sample records |
| 4 | Publish it, then apply it with a constraint |
You need policy turned on (Connect Your OPA), including the
kamiwaza_drafts bundle, and kapi as in
Run Your Own Connector: Before you start.
What a template reads and returns
Input
Kamiwaza sends the same input to every template:
| Field | Holds |
|---|---|
input.records | The records the connector returned, as a list of JSON objects |
input.parameters | The parameters of the constraint being evaluated |
input.subject.attributes | The user's declared attributes, for example {"team": ["ops-leads"], "region": "eu"}, and their marking level as marking_level |
input.resource.urn | The data source |
input.context.verb | browse, search, fetch or query |
There is no user ID, name or group in the input. Write rules on attributes. For who may see a data source at all, use a grant.
Output
A template returns one or both of these sets. Each entry names a record by its
position in input.records, counting from 0:
| Set | Entry | Effect |
|---|---|---|
drop | {"index": <n>, "reason": "<your code>"} | The record is removed |
redact | {"index": <n>, "field": "<name>", "reason": "<your code>"} | The field's value is replaced with null |
mask, pseudonymize and deny are reserved. If a template returns any of
them, the result is withheld rather than released without them.
Rules a template must follow
Kamiwaza checks each template when you start its draft, and refuses one that breaks these rules, with a message that says why:
- The package is
kamiwaza.templates.<name>, and<name>is the name you upload it under: a lowercase letter, then lowercase letters, digits and_. - It reads only
input. A reference todatais refused. - Each output is a set rule:
drop contains {...} if {...}. Any other form, such asdrop := [...], is refused. - It doesn't use these built-ins:
http.send,net.lookup_ip_addrandopa.runtime, which reach outside the OPA;print, which would write records to the OPA's log;crypto.md5,crypto.sha1,crypto.hmac.md5andcrypto.hmac.sha1, which an OPA running in FIPS mode refuses.
- It's 256 KB or smaller.
Start the module with import rego.v1, as the templates Kamiwaza ships do.
Fail closed
Write rules so that missing data drops or redacts, rather than lets a record
through. In Rego, an expression that reads a missing field is undefined, not
false, and not over it doesn't help: "drop if rec.region isn't allowed"
never fires for a record without a region. Read fields that may be missing
with object.get, as the example below does, and test the missing case.
A template that hits a runtime error, such as calling lower on a number, has
its results withheld.
Step 1: Write it
The example drops any record whose region isn't one of the user's regions. The
field and the attribute are parameters, so one template serves several data
sources. Save it as region_match.rego:
package kamiwaza.templates.region_match
import rego.v1
# object.get turns a missing attribute or field into a value that never
# matches, so those records are dropped rather than kept.
user_regions := {r | some r in object.get(input.subject.attributes, input.parameters.attribute, [])}
drop contains {"index": i, "reason": "other_region"} if {
some i, rec in input.records
not object.get(rec, input.parameters.field, null) in user_regions
}
It expects a list attribute (type string[]), declared as in
Restrict a Data Source.
Write the parameter schema, as JSON Schema. Kamiwaza
checks every constraint's parameters against it. Save it as
region_match.schema.json:
{
"type": "object",
"additionalProperties": false,
"required": ["field", "attribute"],
"properties": {
"field": {"type": "string", "minLength": 1},
"attribute": {"type": "string", "minLength": 1}
}
}
Step 2: Start a draft
TEMPLATE=region_match
jq -n --rawfile rego region_match.rego --slurpfile schema region_match.schema.json \
'{rego: $rego, parameters_schema: $schema[0], change_note: "Drop records outside the user regions"}' \
| kapi POST "/authz/policies/templates/$TEMPLATE/drafts" -d @- > draft.json
DRAFT=$(jq -r '.draft_id // empty' draft.json)
[ -n "$DRAFT" ] && echo "draft $DRAFT" || cat draft.json
If a rule above is broken, the response says which one, and no draft is made.
Your OPA downloads the draft and compiles it. Wait for the result:
for i in $(seq 1 12); do
kapi GET "/authz/policies/drafts/$DRAFT" > draft.json
STATUS=$(jq -r .status draft.json); echo "$STATUS"
[ "$STATUS" = pending ] || break; sleep 5
done
jq '.errors' draft.json
Check: the status is compiled. If it's failed, errors lists each
compiler error with its line and column. Fix the file and start a new draft.
Step 3: Try it on sample records
Give the parameters a constraint would set, and a user's attributes:
kapi POST "/authz/policies/drafts/$DRAFT/dry-run" -d '{
"parameters": {"field": "region", "attribute": "region"},
"records": [
{"id": 1, "region": "eu"},
{"id": 2, "region": "us"},
{"id": 3}
],
"subject_attributes": {"region": ["eu"]}
}' | jq .decision.drop
Check: records 1 and 2 (the second and third) are dropped with reason
other_region, and record 0 is kept. Try a user with no region too: every
record should be dropped.
If the dry run returns "hasn't loaded this draft yet", one of your OPA replicas hasn't downloaded it yet. Run it again.
Step 4: Publish it
kapi POST "/authz/policies/drafts/$DRAFT/promote" -d '{}' | jq '{revision, template: .template | {name, version}}'
The template is now live, but it does nothing until a constraint uses it.
Create one as in Restrict a Data Source,
with "template": "region_match" and parameters such as
{"field": "region", "attribute": "region"}.
Change or retire a template
| Change | How |
|---|---|
| New Rego | Start a draft under the same name (steps 2 to 4). The new version applies to every constraint that uses the template. Promoting is refused if a live constraint's parameters don't fit the new schema. |
| Roll back | Start a draft with {"from_version": <n>} instead of rego, and promote it. |
| Retire | kapi POST "/authz/policies/templates/$TEMPLATE/retire" -d '{"change_note": "<why>"}'. Refused while a live constraint uses it: retire or change those constraints first. |
To see the versions:
kapi GET "/authz/policies/templates/$TEMPLATE/versions" | jq '.[] | {version, created_at, created_by, change_note}'