§ Reference · Policies
The plan document in a policy
What a policy is unified with. How absent, null and unknown values behave, and what differs when a saved plan is applied.
On this page
A policy is unified with the plan document, the JSON that pravo plan --json prints. All fields are in The plan document. The printed document lists the nodes the plan ran; a policy is judged against the document that pravo plan --json --include-skipped prints, which has an entry for every node of the specification. This page says what matters when a rule reads them.
| Rule about | Read | Always present |
|---|---|---|
| What the plan does to a resource | changes[].action |
yes |
| What kind of object it is | changes[].mode, changes[].type |
yes |
| Which object | changes[].addr, changes[].name |
yes |
| A value after the change | changes[].after |
for create, update, replace, read |
| A value before the change | changes[].before |
for update, replace, delete |
| What forces a replacement | changes[].requires_replace |
for replace |
| Who depends on the object | changes[].dependents |
for a pending change, when there are any |
| The size of the plan | summary.add, .change, .destroy, .read, .blast_radius |
yes |
| How much of the graph was walked | stats.* |
yes |
| The outputs | outputs.<name> |
yes |
mode is managed, data, ephemeral or call. A value that pravo reads itself (pravo.#Env, pravo.#SecretEnv, pravo.#RemoteOutputs) produces no entry, so a rule over it never fires. action is create, update, replace, delete, read, open, call, move or no-op.
A replace is its own action. A policy that forbids delete alone still allows a resource to be destroyed and created again.
Work in an empty directory. The root is infra, and the policies are in policy.
package infra
import rnd "pravo.dev/providers/hashicorp/random"
$random: rnd.#Provider & {$version: "~> 3.6"}
pet: $random.#Resource.random_pet & {
length: 2
keepers: owner: "platform"
}
db_password: $random.#Resource.random_password & {
length: 8
special: false
}
Absent, null and unknown
A value that a policy requires can fail to be a value in three ways. None of them is the same as a value that is present.
| State | In the document | Example |
|---|---|---|
| Absent | The field is not there. | before of a create. after of a delete. Both for a no-op. A boolean that is false. |
| Null | The field is there with the value null. |
An attribute that is not set: the prefix of the pet. |
| Unknown | The struct {"$unknown": true}. |
A value that the provider computes at the apply: the id of the pet. |
An unknown value is a struct, not a string or a number. It conflicts with every constraint on a string or a number.
{"before":null,"after":{"id":{"$unknown":true},"keepers":{"owner":"platform"},"length":2,"prefix":null,"separator":"-"}}
before is absent, so jq prints null for it. In after, prefix is null and id is {"$unknown": true}.
A rule that reads a field which may be absent declares it optional and tests it first. See Read an absent field only after a presence test.
The document is not masked
The text of a plan hides sensitive attributes. The document does not. Sensitive attributes, and the outputs marked sensitive, appear in it as they are. Apply the root, then change the length of the password from 8 to 9:
Apply complete! Resources: 2 added, 0 changed, 0 destroyed. [txn <id>]
7: ~ bcrypt_hash: (sensitive) -> (sensitive)
9: ~ length: 8 -> 9 # forces replacement
17: ~ result: (sensitive) -> (sensitive)
The text shows (sensitive). The document shows the value:
{
"addr": "db_password",
"action": "replace",
"before": {
"length": 8,
"result": "<secret>"
}
}
Do not write rules over secrets. When a rule fails, the refusal quotes the value that failed it. Do not commit a plan document as a test fixture without scrubbing it.
Empty plans
A plan whose specification declares no resources has changes: [], an empty list and never null. A plan with resources and nothing to do has an entry for each of them, with the action no-op, in the document that a policy is judged against. An empty plan, and a plan of no-ops, satisfies a prohibition, because there is nothing to violate.
// A managed resource is neither deleted nor replaced.
changes: [...{
addr: string
mode: string
action: string
if mode == "managed" && (action == "delete" || action == "replace") {
error("\(addr): \(action) is not allowed")
}
}]
package empty
import "pravo.dev/pravo"
region: pravo.#Env & {name: "AWS_REGION", default: "eu-west-1"}
where: pravo.#Output & {value: region.value}
No changes. Your infrastructure matches the configuration.
Graph: planned 0 of 1.
Outputs:
where: "eu-west-1"
Policy ../policy/no-destroy.cue (sha256:0c23a31e78bd) is satisfied.
{"changes":[],"summary":{"add":0,"change":0,"destroy":0,"read":0,"blast_radius":0}}
With the resources of infra applied and nothing changed, the plan ran none of them: it did not send them to the provider, and the printed document lists none. The document with --include-skipped, which is the one a policy sees, has an entry for each, with the action no-op:
[]
[{"addr":"pet","action":"no-op","skipped":true},{"addr":"db_password","action":"no-op","skipped":true}]
{"add":0,"change":0,"destroy":0,"read":0,"blast_radius":0}
Policy ../policy/no-destroy.cue (sha256:0c23a31e78bd) is satisfied.
The entry of a node that the plan left alone has addr, mode, type, name, action and skipped, and nothing else: no reasons, no dependencies, no before and no after. The entries are in the order of the specification, so a rule over the position of an entry reads the same document whatever the plan ran. A rule over the type or the addr of a node reaches it. A rule over attributes judges only the resources that a change touches. See Operating policies for finding the resources that already break a rule.
What apply judges
pravo apply <planfile> --policy FILE builds the document from the plan file when it is applied, and judges the changes that remain. The file has no entry for a node that the plan left alone. The document has one for it, built from the record in the state, as in pravo plan --include-skipped. These fields describe the apply, not the command that made the plan file: created, actor, reason, git_revision and trace_id. A plan made with --actor agent and applied with --actor ci is judged as actor: "ci". The labels are the caller's words in any case, and a rule over them binds nothing.
The entries with the actions open and call are not written to a saved plan. A rule over mode == "ephemeral" or mode == "call" is judged when the plan is made and not again when the saved plan is applied. The document that plan and apply judge never carries reservation.
See also
- The plan document lists every field.
- Writing policies shows the rules that read these fields.
- Secrets and ephemeral values says what the document holds.