mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
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:
@@ -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 @@
|
|||||||
|
{}
|
||||||
@@ -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 @@
|
|||||||
|
{}
|
||||||
@@ -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 @@
|
|||||||
|
{}
|
||||||
@@ -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 @@
|
|||||||
|
{}
|
||||||
@@ -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 @@
|
|||||||
|
{}
|
||||||
@@ -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')} />
|
||||||
|
|||||||
Reference in New Issue
Block a user