§ How-to · Share and protect
Approve a plan
Make a plan wait for the approval of another member before it is applied. Write the rule, store the plan, approve it with a second key, apply it by its id, and see what an admin may skip and what a change to the rule itself needs.
On this page
A plan that destroys a database should not run on one person's say-so. A policy can ask that a plan is approved by other members before it is applied, and the apply refuses until it is. An approval is a signature by a key of the readers file, so it needs a sealed state with roles. This page writes the rule, stores a plan that it catches, approves the plan with a second key and applies it, and then shows what an admin may skip and what a change to the rule itself needs.
Note
The state is a local directory, sealed with --allow-local so that the page needs no bucket. The three people, ada, dan and eve, are three identity directories on one machine, and PRAVO_CONFIG_DIR picks one. Each command is written as the person who runs it; a command without a prefix is ada's.
1. Start from a sealed state with roles
The root is team/, with one resource. Its state is sealed to ada, an admin, and to dan and eve, two writers, the roles are pinned, and ada has applied it once. Give people roles sets this up step by step.
package team
import rnd "pravo.dev/providers/hashicorp/random"
$random: rnd.#Provider & {$version: "~> 3.6"}
pet: $random.#Resource.random_pet & {length: 2}
ada role:admin
dan role:writer
eve role:writer
Sealed on · format 3 · 3 readers (ada dan eve) · recovery none
Roles on · 1 admin · 2 writer · 0 reader · pinned, version 1 in the state · you are an admin
2. Write the rule
A policy is a node of the root whose rule is a CUE value that every plan has to fit, as Write and roll out policies shows. This one asks for an approval of any plan that destroys something:
package team
import "pravo.dev/pravo"
second_opinion: pravo.#Policy & {
description: "A plan that destroys a resource is applied once another writer has approved it."
rule: {
{summary: destroy: 0} | {summary: destroy: >0, approvals: {min: 1}}
}
}
The | makes the rule a choice between two shapes, and a plan fits the rule when it fits one of them. The left shape fits a plan whose summary destroys nothing, and asks for nothing more. The right shape fits a plan that destroys one resource or more, and approvals: {min: 1} is what it asks for: one approval, by a key whose role is writer or above, other than the key that stored the plan. A replacement destroys the old object, so it counts. The rule is written as a choice because a rule cannot say if about the document as a whole; Writing policies lists the shapes that work.
approvals has three fields, and the two that are left out take their defaults:
approvals: {
min: 2 // how many distinct keys have to approve
role: "admin" // the weakest role that counts: "writer", the default, or "admin"
self: true // whether the key that stored the plan may approve it; false by default
}
When several policies ask, the strongest wins: the largest min, the highest role, and the author counts only if every one of them allows it.
3. Add the policy
Ada applies. The plan that adds a policy is not judged by it, and the policy is in force from this apply on.
Policies: +1 new, ~0 changed, -0 removed.
Policies: +1 new, ~0 changed, -0 removed.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed. [txn <id>]
4. Store a plan that the rule catches
Dan removes the resource and stores the plan. At plan time the policy is satisfied: the approvals are counted at the apply, so that they can be given in between. His own apply is refused, because nobody has approved yet and his own key does not count.
Policies: 1 satisfied.
Stored the plan as plan-<id>; unless it is applied it is removed after 7 days. To apply exactly these actions, run:
exit=5
Error: the plan plan-<id> needs 1 approval by a writer or an admin (second_opinion@1) and has 0. The key that stored the plan does not count.
ID in the shell stands for the id that pravo printed. The approvals belong to the stored plan, so a plan that a rule asks approvals for is stored: pravo apply of a plan file, or without a plan, refuses such a plan too, because a file has nowhere to keep an approval.
5. Approve it with a second key
Eve reads the plan first. pravo plan show ID prints it, and the Plans section of the viewer lays it over the graph. Then she approves.
Approved plan-<id> as eve.
Stored plan plan-<id> by dan, just now (pending)
approved: eve (writer)
An approval is a signature over the digest of the plan, by eve's key, kept in the state next to the plan. It holds for this plan alone: a plan made again, from the same change, needs approvals of its own. Whether eve's key counts is decided at the apply, from the readers file of the checkout: a key that was demoted to a reader, or taken off the file, stops counting.
6. Apply it
Apply complete! Resources: 0 added, 0 changed, 1 destroyed. [txn <id>]
plan-<id> applied dan just now +0 ~0 -1 applied just now
The transaction is signed by dan, who applied, and the plan keeps eve's approval, so the state says who asked, who agreed and who ran it.
7. What an admin may skip
An admin needs no approval, so a state with one member works as it did before the rule, and a readers file without roles makes every key an admin. Ada puts the resource back, dan tries the same removal with a plain apply, and ada runs it.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <id>]
exit=5
Error: the plan needs 1 approval by a writer or an admin (second_opinion@1), and approvals belong to a stored plan: make it with plan --store, have it approved with "pravo plan approve" and apply it by its id
Apply complete! Resources: 0 added, 0 changed, 1 destroyed. [txn <id>]
8. Change the rule
Dan rewrites the rule so that it asks for nothing. A writer may edit a policy, and the plan that carries the edit changes no resource, so no rule judges it. A change to a policy that asks for approvals therefore needs one approval by an admin, from a key other than the one that stored the plan, so that a writer cannot end the rule on their own.
package team
import "pravo.dev/pravo"
second_opinion: pravo.#Policy & {
description: "A plan that destroys a resource is applied once another writer has approved it."
rule: {
summary: destroy: >=0
}
}
Policies: +0 new, ~1 changed, -0 removed.
Stored the plan as plan-<id>; unless it is applied it is removed after 7 days. To apply exactly these actions, run:
exit=5
Error: the plan plan-<id> needs 1 approval by an admin (a change to second_opinion@1) and has 0. The key that stored the plan does not count.
Approved plan-<id> as eve.
exit=5
Error: the plan plan-<id> needs 1 approval by an admin (a change to second_opinion@1) and has 0. The key that stored the plan does not count.
Eve's approval is recorded and does not count, because the rule that is being changed asks for an admin. Ada approves, and dan applies:
Approved plan-<id> as ada.
Policies: +0 new, ~1 changed, -0 removed.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed. [txn <id>]
approved: eve (writer), ada (admin)
Removing the policy altogether is an admin's apply: a writer's apply that removes a policy is refused before it changes anything. Adding a policy needs nothing, since a new rule can only ask for more.
9. In a pipeline
The plan job stores the plan and prints its id: pravo plan --store --json, and the field stored.id of the document. A person reads it, with pravo plan show ID or in the viewer, and approves it from their own machine with pravo plan approve ID. The apply job applies by id, and exit code 5 with needs N approvals means that the job runs again later, or that the pipeline waits for the person. The identity of the apply job is a writer. No job is an admin, because an admin's apply skips the approvals. Run pravo in a pipeline has the jobs.
What an approval does not do
- There is no command that withdraws an approval. A plan that should not be applied after all is left to expire, and a plan made again from the same change needs approvals of its own.
mincounts distinct keys, not people. A member who has two identities on the readers file, as Seal the state allows, could approve twice. Where approvals matter, give each person one line, or setminwith that in mind.- The viewer shows a plan as pending until it is applied, and does not count approvals:
pravo applydoes, under the locks. - A state that is not sealed has no approvals, because it has no signatures. A readers file without roles makes every key an admin, and an admin needs none.
What next
- Give people roles sets the roles that the approvals count.
- Write and roll out policies tests a rule on fixtures before it is in force.
- Operating policies is the reference for
approvals, andpravo plan approvefor the command. - Stored plans and approvals explains what an approval is and where it lives.