Skip to main content

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.

StepWhat
1Write the template and its parameter schema
2Start a draft, and wait for your OPA to compile it
3Try it on sample records
4Publish 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:

FieldHolds
input.recordsThe records the connector returned, as a list of JSON objects
input.parametersThe parameters of the constraint being evaluated
input.subject.attributesThe user's declared attributes, for example {"team": ["ops-leads"], "region": "eu"}, and their marking level as marking_level
input.resource.urnThe data source
input.context.verbbrowse, 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:

SetEntryEffect
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 to data is refused.
  • Each output is a set rule: drop contains {...} if {...}. Any other form, such as drop := [...], is refused.
  • It doesn't use these built-ins:
    • http.send, net.lookup_ip_addr and opa.runtime, which reach outside the OPA;
    • print, which would write records to the OPA's log;
    • crypto.md5, crypto.sha1, crypto.hmac.md5 and crypto.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​

ChangeHow
New RegoStart 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 backStart a draft with {"from_version": <n>} instead of rego, and promote it.
Retirekapi 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}'