docs(policy-reference): add examples for common built-ins (#8913)

## Description

Incremental pass on #3786: add `PlaygroundExample` blocks for a handful
of frequently used built-ins that had no examples yet.

| Built-in | Example |
|---|---|
| `units.parse` | Compare container memory limits expressed with
different unit suffixes |
| `object.get` | Read an optional label with a default (`env` → `dev`) |
| `startswith` | Restrict HTTP paths to `/api/v1/` |
| `sprintf` | Build a deny message with user/role/action/resource |
| `intersection` | Check required OAuth-style scopes are present |
| `array.concat` | Merge a base registry allowlist with tenant extras |

Each example follows the existing layout under
`docs/docs/policy-reference/_examples/` (`policy.rego`, `input.json`,
`intro.md`, `output.json`, …). Outputs were generated with `opa eval`
(same approach as `docs/bin/eval-examples.sh`).

`units.parse` was suggested in the issue thread as a good starting
point.

## Checklist

- [x] I have read the [contribution
guidelines](https://www.openpolicyagent.org/docs/latest/contributing/).
- [x] I have added/updated tests for my change (docs-only; examples
evaluate cleanly with `opa eval`).
- [x] All new/updated docs follow the existing style of nearby examples
(`count`, `sum`, `contains`, …).

Closes nothing by itself — this is partial progress on #3786.

---------

Signed-off-by: Dean Chen <51218137+locker95@users.noreply.github.com>
Signed-off-by: Dean Chen <862469039@qq.com>
This commit is contained in:
Dean Chen
2026-08-06 07:15:22 +05:00
committed by GitHub
parent 346f288c32
commit af3678f6cf
47 changed files with 347 additions and 0 deletions
@@ -0,0 +1,6 @@
{
"showInput": true,
"showData": true,
"showTitles": false,
"titleSize": 4
}
@@ -0,0 +1,6 @@
{
"base_registries": [
"registry.internal/prod",
"ghcr.io/example"
]
}
@@ -0,0 +1,6 @@
{
"extra_registries": [
"registry.internal/staging"
],
"image": "docker.io/library/nginx:1.27"
}
@@ -0,0 +1,5 @@
<!-- markdownlint-disable MD041 -->
`array.concat` appends one array to another. Policies often keep a fixed
base list (for example default registries) and extend it with values from
input or data for a particular tenant or environment.
@@ -0,0 +1,11 @@
{
"allow": false,
"allowed_registries": [
"registry.internal/prod",
"ghcr.io/example",
"registry.internal/staging"
],
"deny": [
"image \"docker.io/library/nginx:1.27\" is not from an allowed registry: [\"registry.internal/prod\", \"ghcr.io/example\", \"registry.internal/staging\"]"
]
}
@@ -0,0 +1,18 @@
package play
allowed_registries := array.concat(data.base_registries, input.extra_registries)
default allow := false
allow if {
some registry in allowed_registries
startswith(input.image, sprintf("%s/", [registry]))
}
deny contains msg if {
not allow
msg := sprintf(
"image %q is not from an allowed registry: %v",
[input.image, allowed_registries],
)
}
@@ -0,0 +1 @@
Merging a base allowlist with extra entries
@@ -0,0 +1,6 @@
{
"showInput": true,
"showData": false,
"showTitles": false,
"titleSize": 4
}
@@ -0,0 +1,17 @@
{
"workloads": [
{
"name": "frontend",
"labels": {
"app": "web",
"env": "prod"
}
},
{
"name": "scratch",
"labels": {
"app": "jobs"
}
}
]
}
@@ -0,0 +1,9 @@
<!-- markdownlint-disable MD041 -->
Kubernetes objects and API payloads often omit optional fields. `object.get`
reads a key (or a nested path) and returns a default when it is missing, so
the rest of the policy does not need extra existence checks. Using a path
array also covers the case where an intermediate field like `labels` is
undefined.
Here, workloads without an `env` label are treated as `dev`.
@@ -0,0 +1,9 @@
{
"deny": [
"production workload \"frontend\" is missing labels.team"
],
"envs": {
"frontend": "prod",
"scratch": "dev"
}
}
@@ -0,0 +1,15 @@
package play
# object.get(object, key, default) — key may also be a path array, which
# still returns the default when an intermediate field (like labels) is missing.
env_of(workload) := object.get(workload, ["labels", "env"], "dev")
# Only production workloads need a team label.
deny contains msg if {
some w in input.workloads
env_of(w) == "prod"
not w.labels.team
msg := sprintf("production workload %q is missing labels.team", [w.name])
}
envs := {w.name: env_of(w) | some w in input.workloads}
@@ -0,0 +1 @@
Reading optional labels with a default
@@ -0,0 +1,6 @@
{
"showInput": true,
"showData": false,
"showTitles": false,
"titleSize": 4
}
@@ -0,0 +1,4 @@
{
"granted": ["read:orders", "write:orders", "read:profile"],
"required": ["read:orders", "write:payments"]
}
@@ -0,0 +1,6 @@
<!-- markdownlint-disable MD041 -->
`intersection` returns the values common to every set you pass in. A typical
use is comparing a caller's granted scopes with the scopes an endpoint
requires: if the intersection is smaller than the requirement, something is
missing.
@@ -0,0 +1,21 @@
{
"allow": false,
"deny": [
"missing required scopes: {\"write:payments\"}"
],
"granted": [
"read:orders",
"read:profile",
"write:orders"
],
"missing": [
"write:payments"
],
"present": [
"read:orders"
],
"required": [
"read:orders",
"write:payments"
]
}
@@ -0,0 +1,17 @@
package play
granted := {s | some s in input.granted}
required := {s | some s in input.required}
present := intersection({granted, required})
missing := required - present
default allow := false
allow if count(missing) == 0
deny contains msg if {
count(missing) > 0
msg := sprintf("missing required scopes: %v", [missing])
}
@@ -0,0 +1 @@
Checking that required scopes are present
@@ -0,0 +1,6 @@
{
"showInput": true,
"showData": false,
"showTitles": false,
"titleSize": 4
}
@@ -0,0 +1,6 @@
{
"user": "alice",
"role": "guest",
"action": "delete",
"resource": "orders/42"
}
@@ -0,0 +1,5 @@
<!-- markdownlint-disable MD041 -->
`sprintf` formats a string with values from the policy. Admission and
authorization policies use it so users see _which_ field failed and _what_
value was rejected, not only a bare `false`.
@@ -0,0 +1,5 @@
{
"deny": [
"user alice with role guest cannot delete orders/42"
]
}
@@ -0,0 +1,11 @@
package play
# Guests may read, but nothing else.
deny contains msg if {
input.role == "guest"
input.action != "read"
msg := sprintf(
"user %v with role %v cannot %v %v",
[input.user, input.role, input.action, input.resource],
)
}
@@ -0,0 +1 @@
Building a clear deny message
@@ -0,0 +1,6 @@
{
"showInput": true,
"showData": false,
"showTitles": false,
"titleSize": 4
}
@@ -0,0 +1,3 @@
{
"path": "/api/v2/users"
}
@@ -0,0 +1,7 @@
<!-- markdownlint-disable MD041 -->
When a policy only cares about the start of a string — for example an HTTP
path or a registry prefix — `startswith` is clearer (and usually safer)
than a loose `contains` check.
This example allows only paths under `/api/v1/`.
@@ -0,0 +1,6 @@
{
"allow": false,
"deny": [
"path \"/api/v2/users\" is outside /api/v1/"
]
}
@@ -0,0 +1,10 @@
package play
default allow := false
allow if startswith(input.path, "/api/v1/")
deny contains msg if {
not allow
msg := sprintf("path %q is outside /api/v1/", [input.path])
}
@@ -0,0 +1 @@
Restricting requests to an API path prefix
@@ -0,0 +1,6 @@
{
"showInput": true,
"showData": false,
"showTitles": false,
"titleSize": 4
}
@@ -0,0 +1,13 @@
{
"namespace_memory_limit": "1Gi",
"containers": [
{
"name": "app",
"memory_limit": "512Mi"
},
{
"name": "batch",
"memory_limit": "2Gi"
}
]
}
@@ -0,0 +1,6 @@
<!-- markdownlint-disable MD041 -->
Container specs often express memory with different suffixes (`Mi`, `Gi`,
bare numbers). `units.parse` turns those strings into numbers so a policy
can compare them. Here, a container is rejected when its memory limit is
higher than the namespace cap.
@@ -0,0 +1,6 @@
{
"deny": [
"container \"batch\" memory limit 2Gi exceeds namespace cap 1Gi"
],
"max_memory": 1073741824
}
@@ -0,0 +1,14 @@
package play
# Cap for the whole namespace, normalized to a number.
max_memory := units.parse(input.namespace_memory_limit)
# Containers whose limit is above the namespace cap.
deny contains msg if {
some c in input.containers
units.parse(c.memory_limit) > max_memory
msg := sprintf(
"container %q memory limit %s exceeds namespace cap %s",
[c.name, c.memory_limit, input.namespace_memory_limit],
)
}
@@ -0,0 +1 @@
Comparing memory limits with different units
@@ -3,3 +3,13 @@ title: Array Built-ins
sidebar_label: Arrays sidebar_label: Arrays
--- ---
<BuiltinTable category={"array"}/> <BuiltinTable category={"array"}/>
## Examples
### `array.concat`
`array.concat` returns a new array with the elements of the second array
appended to the first. Policies use it to extend a base list — for example
default registries or hosts — with extra values from input or data.
<PlaygroundExample dir={require.context('../_examples/array/concat/merge-allowlists')} />
@@ -15,3 +15,14 @@ sidebar_label: Objects
in `{ "foo/bar~": "baz" }`. in `{ "foo/bar~": "baz" }`.
- The `json` string `paths` may be an array of string path segments rather than a `/` separated string. For example - The `json` string `paths` may be an array of string path segments rather than a `/` separated string. For example
the path `a/b/c` can be passed in as `["a", "b", "c"]`. the path `a/b/c` can be passed in as `["a", "b", "c"]`.
## Examples
### `object.get`
`object.get` reads a key from an object and returns a default when the key
is missing. The second argument can also be a path array to walk nested
objects. That is useful for optional labels, annotations, or config keys
that callers are allowed to omit.
<PlaygroundExample dir={require.context('../_examples/object/get/optional-labels')} />
@@ -3,3 +3,14 @@ title: Set Built-ins
sidebar_label: Sets sidebar_label: Sets
--- ---
<BuiltinTable category={"sets"}/> <BuiltinTable category={"sets"}/>
## Examples
### `intersection`
`intersection` returns the values that appear in every set of a set-of-sets.
Comparing a caller's granted scopes with the scopes an endpoint requires is
a typical case: anything left after subtracting the intersection from the
requirement is missing.
<PlaygroundExample dir={require.context('../_examples/sets/intersection/required-scopes')} />
@@ -46,3 +46,21 @@ about it in the [keywords section](/docs/policy-reference/keywords/contains).
<PlaygroundExample dir={require.context('../_examples/strings/contains/email-validation')} /> <PlaygroundExample dir={require.context('../_examples/strings/contains/email-validation')} />
<PlaygroundExample dir={require.context('../_examples/strings/contains/content-moderation')} /> <PlaygroundExample dir={require.context('../_examples/strings/contains/content-moderation')} />
### `startswith`
`startswith` reports whether a string begins with a given prefix. Prefer it
over `contains` when the match must be at the front of the value (HTTP paths,
registry prefixes, file paths).
<PlaygroundExample dir={require.context('../_examples/strings/startswith/api-path-prefix')} />
### `sprintf`
`sprintf` builds a string from a format and a list of values. Deny rules use
it to put the failing field and value into the message returned to the caller.
See also the note at the top of this page about how `sprintf` pre-processes
values (for example with `%T`).
<PlaygroundExample dir={require.context('../_examples/strings/sprintf/deny-message')} />
@@ -3,3 +3,17 @@ title: Unit Built-ins
sidebar_label: Units sidebar_label: Units
--- ---
<BuiltinTable category={"units"}/> <BuiltinTable category={"units"}/>
## Examples
### `units.parse`
`units.parse` turns a string with an optional unit suffix into a number.
It accepts decimal SI suffixes (`K`, `M`, `G`, …), binary suffixes
(`Ki`, `Mi`, `Gi`, …), and a lower-case `m` for milli. `m` and `M` are
case-sensitive so milli and mega stay distinct.
A common use is comparing resource limits that arrive with different
suffixes in the same policy check.
<PlaygroundExample dir={require.context('../_examples/units/parse/memory-limits')} />