Skip to content
Technical preview. This site is published for review. Everything on it, including the API, tokens and module protocol, is subject to change.
Policy enforcement

Policy enforcement

Some operations need a governance decision before they proceed. The built-in policy module supplies the decision: its evaluate action returns a verdict, and the workflow definition enforces it. AutoFlow acts on no verdict by itself; the definition decides what each one means, and a person enters only when the verdict asks for one.

Ask for a verdict

evaluate(trigger=, resource=, context=None) is the policy module’s only action. trigger names the event the decision is about, by convention reverse-DNS; resource names what it is about; the optional context is a JSON object string the policies read as input. The result is a dict with decision_id and verdict.

load("module:policy",
     "evaluate", "ALLOW", "DENY", "REQUIRE_APPROVAL", "UNDECIDABLE")

decision = gather(evaluate(
    trigger = "com.gitlab.deploy.requested",
    resource = "projects/%d/environments/%s" % (project_id, environment),
))

Like every action, evaluate returns a future; gather it to get the dict, see Futures and gather. The module exports the four verdicts as variables, so a definition compares against names rather than strings.

Enforce the verdict

VerdictMeaningWhat the workflow definition does
ALLOWThe policies permit the operation.Proceeds.
DENYA policy forbids the operation. No other policy can overturn a deny.Fails closed with fail().
UNDECIDABLEThe policies reached no verdict; nothing is known about the operation.Fails closed, or escalates to a person the same way as REQUIRE_APPROVAL.
REQUIRE_APPROVALThe operation may proceed only if a person approves it.Hands the decision to a person: creates a channel, hands it out, waits for the answer.

Fail closed is the default. Anything that is not ALLOW, and not a verdict the definition explicitly escalates, ends the workflow. The example below does that with two branches: REQUIRE_APPROVAL asks a person, and every other verdict except ALLOW fails the workflow.

Example: approval by a person

This workflow definition asks the policy module whether a deployment may proceed. When the verdict is REQUIRE_APPROVAL it hands a channel to GitLab, suspends until somebody answers, and then creates the deployment. AutoFlow keeps the workflow alive for the whole wait without holding a single goroutine.

load("module:policy", "evaluate", "REQUIRE_APPROVAL", "ALLOW")
load("module:gitlab", "call_api", "post_value")

def main(w, project_id, environment, token):
    headers = {
        "Authorization": "Bearer " + token,
        "Content-Type": "application/json",
    }

    decision = gather(evaluate(
        trigger = "com.gitlab.deploy.requested",
        resource = "projects/%d/environments/%s" % (project_id, environment),
    ))
    if decision["verdict"] == REQUIRE_APPROVAL:
        reply = channel()
        gather(post_value(
            "/api/v4/projects/%d/deploy_approvals" % project_id,
            value = {"environment": environment, "reply": reply},
            headers = headers,
        ))
        if gather(reply, timeout = 3 * 24 * time.hour) != "approved":
            fail("deployment not approved")
    elif decision["verdict"] != ALLOW:
        fail("deployment denied by policy")

    status, _, _, err = gather(call_api(
        "POST",
        "/api/v4/projects/%d/deployments" % project_id,
        headers = headers,
        body = json.encode({"environment": environment}),
    ))
    return status
    sequenceDiagram
    participant F as Workflow
    participant R as AutoFlow
    participant G as GitLab
    participant H as Human
    F->>R: evaluate(trigger, resource)
    R-->>F: verdict REQUIRE_APPROVAL
    F->>R: post_value(path, value with channel)
    R->>G: POST deploy_approvals {value, channel_tokens}
    G->>H: approval request
    Note over F,R: Workflow suspended, holding no resources
    H->>G: approve
    G->>R: SendToWorkflowChannel(channel_token, workflow_token, "approved")
    R-->>F: gather(reply) returns "approved"
    F->>R: call_api("POST", deployments)
    R->>G: POST /api/v4/projects/:id/deployments
  

Create a channel

channel() creates a stream the workflow can receive on. The workflow itself cannot send to it; only a module the channel was passed to, or whoever the module hands it on to, can. That is what makes it a safe handle to give away.

Hand it to the outside

post_value(path, value=, headers=None) POSTs a JSON body to a GitLab endpoint. The channel sits inside value like any other value. Because the invocation carries a channel, the module exchanges the channel’s token, which is minted for the module itself, for one that binds no principal, and puts it in the body next to the value:

{
  "value": {"dict_value": {"key_values": [
    {
      "key": {"string_value": "environment"},
      "val": {"string_value": "production"}
    },
    {
      "key": {"string_value": "reply"},
      "val": {"channel_value": {"name": "..."}}
    }
  ]}},
  "channel_tokens": [{"channel_name": "...", "token": "<JWT>"}]
}

Content-Type is always application/json and an Idempotency-Key header is always sent, so a retried POST is recognizable. Like call_api, post_value returns the (status, headers, body, error) tuple; the definition above gathers it without inspecting it, which is enough to make the call happen.

Wait

gather(reply, timeout = 3 * 24 * time.hour) blocks until a value arrives on the channel or three days pass. On timeout it returns None, the default timeout_value, and consumes nothing. None != "approved", so a timeout is a rejection. During the wait the workflow is suspended; it holds no goroutine, no connection and no memory. See Durable waits.

Act

Once the answer is "approved", or the verdict was ALLOW to begin with, the workflow creates the deployment with call_api and returns the HTTP status as the workflow result. A non-2xx status is data in the tuple, so a real workflow definition inspects status and err before returning.

The receiving side

The endpoint that receives the posted value is responsible for showing the request to a person and, once they decide, calling the SendToWorkflowChannel RPC on AutoFlow’s API listener. The request has five fields:

{
  "idempotency_key": "deploy-approval-4711",
  "channel_token": "<token from the posted body>",
  "value": {"string_value": "approved"},
  "workflow_token": "<token StartWorkflow returned>",
  "namespace_id": 1
}
  • idempotency_key deduplicates the message within the workflow. A retry with the same key delivers at most once; a new key is a second delivery.
  • channel_token is taken from channel_tokens in the posted body, opaque material to forward.
  • value must not contain a channel and must marshal to at most 64 KiB.
  • workflow_token is the capability StartWorkflow returned to whoever started the workflow. The channel token alone carries no send. The recipient must resolve the workflow token from its own records, never from the posted body: the body arrives without any AutoFlow credential, so nothing in it is attested.

The send is fire-and-forget. A successful response means AutoFlow accepted the value; the workflow sees it on its next round. A workflow that already ended refuses the send with FAILED_PRECONDITION.

Where this stands today

  • The /api/v4/projects/:id/deploy_approvals endpoint in the definition is illustrative. No GitLab endpoint receives posted values and answers on the channel yet; the receiving side has to be built by the integration that starts the workflow.
  • The policy module returns ALLOW for every evaluation until a policy source is wired to it, and decision_id is a zero UUID placeholder. It is only available in an AutoFlow build that links the policy engine; otherwise load("module:policy", ...) fails with policy engine is not enabled.
  • A channel a child workflow hands out does not work yet: the send succeeds and the value is dropped. Create the channel in the workflow that will gather it.
  • The workflow token, channel token and token binding this guide relies on are an interim design. Expect them to change once GATE is available.

Related

Last updated on