Module 18: Static attributes (operator-maintained facts)#
You are in the CPEX tutorial. This module needs the IdP.
Goal: feed policy the facts no token carries, operator decisions maintained in a data file, and read them per request as data.*.
The problem#
Some facts are carried by nothing. Which region a deployment’s data must stay in. Which tools are switched on this week. The org’s default tier. These are operator decisions, known at configuration time and belonging in neither the token nor the application code, unlike the attributes of modules 2 and 7, which arrive with the request.
CPEX provisions them from a plain data file into the data.* namespace. Identity turns a token into subject.*; static provisioning turns a config file into data.*, and predicates read both the same way.
Build it#
A data file of facts (note the top-level data: wrapper). From policies/attributes/controls.yaml:
data:
org:
data_region: eu
controls:
tools:
get_compensation: { enabled: false } # frozen by operations
search_repos: { enabled: true }List it under global.apl.attribute_files, and read it in a rule. From policies/m18.yaml:
global:
apl:
attribute_files:
- examples/tutorial/policies/attributes/controls.yaml # relative to repo root
routes:
- tool: get_compensation
authentication: [keycloak]
authorization:
pre_invocation:
- "require(authenticated)"
- when: "data.controls.tools.get_compensation.enabled == false"
do:
- "deny('get_compensation is disabled by operations', 'ops.tool_disabled')"The whole file flattens into the bag: data.org.data_region, data.controls.tools.get_compensation.enabled, and so on. Multiple files deep-merge in order, and the merge is fail-fast: two files setting the same leaf differently is a load error, and a file missing its data: wrapper is rejected. A config mistake stops startup rather than quietly mis-routing.
The tree holds literal values only, with no conditionals, computed fields, or cross-references. A data document has no syntax for logic, so the static layer cannot grow into a second, shadow policy engine. It provisions the facts; APL decides with them.
Run it#
Run this one from the repo root: attribute_files paths resolve against the working directory.
cargo run -p cpex-tutorial --example m18_attributes▸ alice → get_compensation (data.controls says this tool is disabled)
✗ DENIED [ops.tool_disabled] get_compensation is disabled by operations
▸ alice → search_repos (data.controls says this tool is enabled)
✓ ALLOWED { ... }The same caller reached one tool and was refused another. Nothing about alice changed between the two calls; the outcome came from a fact in a file, read as data.*.
Reading the tree per caller#
This module reads fixed paths, but a path can also be indexed by a request value, so one rule serves every caller:
- when: "data.tenants[subject.tenant].data_region == 'eu'"
do: [ "taint(eu_resident, session)" ][subject.tenant] is substituted at evaluation time, so the predicate reads data.tenants.<this caller's tenant>.data_region. A missing value resolves to absent and the predicate is false, so a require on it fails closed. Static Attributes has the runnable version, along with the restrict fields that take a data.* reference the same way.
Try it#
- Flip a switch. Set
search_repos.enabled: falseincontrols.yamland re-run. Expect: now both tools are refused, and you changed behavior by editing a data file, touching no policy and no code. - Break the merge. Add a second file to
attribute_filesthat setsdata.org.data_regionto a different value. Expect: a load-time error, because the merge is fail-fast, not last-wins. - Forget the wrapper. Remove the top-level
data:key from the file. Expect: rejected at load, because the file must declaredata:.
Checkpoint#
How is data.* different from subject.*?
Why not just put the flag in the route?
Go deeper#
- Static Attributes for the full
data.*model, merge rules, and theAttributeSourcetrait (etcd / DB / ConfigMap sources). - Backend Restriction for where
data.*references feed per-caller routing constraints.
Next#
Capstone: reconstruct the full scenario end to end.