Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,27 @@ project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Evaluated Rules in Decision Logs and API Responses
### Rule Labels in Decision Logs

Rules can now be annotated with a metadata `id` field. When any loaded policy contains
rules with `id` annotations, the IDs of successfully evaluated rules are automatically
included in decision log events (as `ids`). Additionally, the Data API supports a `?ids`
query parameter to include the same information directly in the response payload.
Duplicate IDs from functions called multiple times are suppressed.
Rule annotations now support a `labels` field. Labels from all successfully evaluated
rules are collected and included in each decision log entry as a top-level `rule_labels`
array. Each element preserves the label map from one evaluated rule.

```rego
# METADATA
# id: allow-admin
# labels:
# severity: low
# team: platform
allow if input.role == "admin"
```

Modules containing `id` annotations will have metadata parsing enabled automatically.
When external rule sources are registered, rule tracking is always enabled so that
externally-provided rules with `id` annotations are recorded.
The resulting decision log entry will contain:

```json
{"rule_labels": [{"severity": "low", "team": "platform"}]}
```

The runtime now processes metadata annotations by default.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This struck me as an acceptable compromise. It's a limitation we can deal with if it really is something blocking people.

## 1.16.1

Expand Down
2 changes: 1 addition & 1 deletion cmd/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ func processWatcherUpdate(ctx context.Context, testParams testCommandParams, pat

var loadResult *initload.LoadPathsResult

err := pathwatcher.ProcessWatcherUpdateForRegoVersion(ctx, testParams.RegoVersion(), paths, removed, store, filter, testParams.bundleMode, false,
err := pathwatcher.ProcessWatcherUpdateForRegoVersion(ctx, ast.ParserOptions{RegoVersion: testParams.RegoVersion(), ProcessAnnotation: true}, paths, removed, store, filter, testParams.bundleMode, false,
func(ctx context.Context, txn storage.Transaction, loaded *initload.LoadPathsResult) error {
if len(loaded.Files.Documents) > 0 || removed != "" {
if err := store.Write(ctx, txn, storage.AddOp, storage.RootPath, loaded.Files.Documents); err != nil {
Expand Down
43 changes: 21 additions & 22 deletions docs/docs/policy-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -2600,18 +2600,18 @@ comment block containing the YAML document is finished

### Annotations

| Name | Type | Description |
| ------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| scope | string; one of `package`, `rule`, `document`, `subpackages` | The scope for which the metadata applies. Read more in the [Metadata Scope section below](#metadata-scope). |
| `id` | string | A unique identifier for the rule, used to track evaluated rules in decision logs. Read more in the [Metadata ID section below](#metadata-id). |
| `title` | string | A human-readable name for the annotation target. Read more in the [Metadata Title section below](#metadata-title). |
| `description` | string | A description of the annotation target. Read more in the [Metadata Description section below](#metadata-description). |
| `related_resources` | list of URLs | A list of URLs pointing to related resources/documentation. Read more in the [Metadata Related Resources section below](#metadata-related_resources). |
| `authors` | list of strings | A list of authors for the annotation target. Read more in the [Metadata Authors section below](#metadata-authors). |
| `organizations` | list of strings | A list of organizations related to the annotation target. Read more in the [Metadata Organizations section below](#metadata-organizations). |
| `schemas` | list of object | A list of associations between value paths and schema definitions. Read more in the [Metadata Schemas section below](#metadata-schemas). |
| `entrypoint` | boolean | Whether or not the annotation target is to be used as a policy entrypoint. Read more in the [Metadata Entrypoint section below](#metadata-entrypoint). |
| `custom` | mapping of arbitrary data | A custom mapping of named parameters holding arbitrary data. Read more in the [Metadata Custom section below](#metadata-custom). |
| Name | Type | Description |
| ------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| scope | string; one of `package`, `rule`, `document`, `subpackages` | The scope for which the metadata applies. Read more in the [Metadata Scope section below](#metadata-scope). |
| `labels` | mapping of key-value pairs | Arbitrary labels attached to a rule, recorded in decision logs when the rule is evaluated. Read more in the [Metadata Labels section below](#metadata-labels). |
| `title` | string | A human-readable name for the annotation target. Read more in the [Metadata Title section below](#metadata-title). |
| `description` | string | A description of the annotation target. Read more in the [Metadata Description section below](#metadata-description). |
| `related_resources` | list of URLs | A list of URLs pointing to related resources/documentation. Read more in the [Metadata Related Resources section below](#metadata-related_resources). |
| `authors` | list of strings | A list of authors for the annotation target. Read more in the [Metadata Authors section below](#metadata-authors). |
| `organizations` | list of strings | A list of organizations related to the annotation target. Read more in the [Metadata Organizations section below](#metadata-organizations). |
| `schemas` | list of object | A list of associations between value paths and schema definitions. Read more in the [Metadata Schemas section below](#metadata-schemas). |
| `entrypoint` | boolean | Whether or not the annotation target is to be used as a policy entrypoint. Read more in the [Metadata Entrypoint section below](#metadata-entrypoint). |
| `custom` | mapping of arbitrary data | A custom mapping of named parameters holding arbitrary data. Read more in the [Metadata Custom section below](#metadata-custom). |

### Metadata `Scope`

Expand Down Expand Up @@ -2665,20 +2665,19 @@ allow if {
message := "welcome!" if allow
```

### Metadata `id`
### Metadata `labels`

The `id` annotation is a string value that uniquely identifies a rule. When
any loaded policy contains rules with `id` annotations (or when external rule
sources are registered), the IDs of successfully evaluated rules are
automatically recorded in decision log events. The `id` can also be returned
in the Data API response using the `?ids` query parameter.

When any module contains a metadata block with an `id` field, annotation
parsing is enabled automatically (even if `ProcessAnnotation` was not set).
The `labels` annotation is a map of arbitrary key-value pairs attached to a
rule (or document). When rules with `labels` are successfully evaluated, their
label sets are automatically recorded in decision log events under the
`rule_labels` field. Labels from document-scoped and rule-scoped annotations
are both collected.

```rego
# METADATA
# id: allow-admin
# labels:
# severity: high
# team: platform
allow if input.role == "admin"
```

Expand Down
60 changes: 0 additions & 60 deletions e2e/cli/evaluated_rules.txtar

This file was deleted.

42 changes: 0 additions & 42 deletions e2e/cli/evaluated_rules_file_logger.txtar

This file was deleted.

33 changes: 33 additions & 0 deletions e2e/cli/rule_labels_disk.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Verify that labels from evaluated rule annotations appear as a top-level
# field in decision logs when policies are loaded from disk (no bundle server).

exec $OPA run --server --addr unix://opa.sock --log-format json --log-level info --set decision_logs.console=true ./policy/ &opa&
retry curl -sf --unix-socket opa.sock 'http://localhost/health'

# Evaluate allow — triggers rule with labels.
exec curl -sf --unix-socket opa.sock -H 'Content-Type: application/json' -d '{"input": {"role": "admin"}}' http://localhost/v1/data/authz/allow
jq stdout '.result == true'

# Evaluate with no match — no labels expected.
exec curl -sf --unix-socket opa.sock -H 'Content-Type: application/json' -d '{"input": {"role": "guest"}}' http://localhost/v1/data/authz/allow
jq stdout '.result == false'

kill -INT opa
wait opa

# Decision log for the admin request should have rule_labels.
jq -s stderr '[.[] | select(.msg == "Decision Log" and .result == true)] | .[0].rule_labels == [{"severity": "low", "team": "platform"}]'

# Decision log for the guest request should have no rule_labels.
jq -s stderr '[.[] | select(.msg == "Decision Log" and .result == false)] | .[0] | has("rule_labels") | not'

-- policy/authz.rego --
package authz

default allow := false

# METADATA
# labels:
# severity: low
# team: platform
allow if input.role == "admin"
60 changes: 60 additions & 0 deletions e2e/cli/rule_labels_file_logger.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Verify that rule labels appear in decision logs written via the
# file_logger plugin (slog-based path).

exec $OPA build -o bundles/bundle.tar.gz --bundle policy/

http_server bundles

exec $OPA run --server --addr unix://opa.sock --log-format json --log-level info --config-file config.yaml &opa&
retry curl -sf --unix-socket opa.sock 'http://localhost/health?bundles'

# POST to v1/data — rule with labels should be evaluated.
exec curl -sf --unix-socket opa.sock -H 'Content-Type: application/json' -d '{"input": {"role": "admin"}}' http://localhost/v1/data/authz/allow
jq stdout '.result == true'

# POST to v1/data — no rules match.
exec curl -sf --unix-socket opa.sock -H 'Content-Type: application/json' -d '{"input": {"role": "guest"}}' http://localhost/v1/data/authz/allow
jq stdout '.result == false'

kill -INT opa
wait opa

# Decision log file should contain rule_labels for the admin request.
jq -s decisions.log '[.[] | select(.rule_labels == [{"severity": "low"}])] | length == 1'

# Decision log file should have an entry without rule_labels for the guest request.
jq -s decisions.log '[.[] | select(.path == "authz/allow" and (.rule_labels == null))] | length == 1'

-- config.yaml --
services:
bundle-server:
url: "http://${HTTP_ADDR}"
bundles:
test:
service: bundle-server
resource: bundle.tar.gz
polling:
min_delay_seconds: 300
max_delay_seconds: 300
plugins:
file_logger:
path: decisions.log
decision_logs:
plugin: file_logger

-- policy/authz.rego --
package authz

default allow := false

# METADATA
# labels:
# severity: low
allow if input.role == "admin"

# METADATA
# labels:
# severity: low
allow if input.role == "editor"

-- bundles/.gitkeep --
72 changes: 72 additions & 0 deletions e2e/cli/rule_metadata_dl.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Verify that labels from evaluated rule annotations appear as a top-level
# field in decision logs.

exec $OPA build -o bundles/bundle.tar.gz --bundle policy/

http_server bundles

exec $OPA run --server --addr unix://opa.sock --log-format json --log-level info --config-file config.yaml &opa&
retry curl -sf --unix-socket opa.sock 'http://localhost/health?bundles'

# Evaluate allow — triggers allow-admin rule.
exec curl -sf --unix-socket opa.sock -H 'Content-Type: application/json' -d '{"input": {"role": "admin"}}' http://localhost/v1/data/authz/allow
jq stdout '.result == true'

# Evaluate violations — triggers both violation rules; labels collected per-rule.
exec curl -sf --unix-socket opa.sock -H 'Content-Type: application/json' -d '{"input": {"role": "admin", "dept": "eng"}}' http://localhost/v1/data/authz/violations
jq stdout '.result | length == 2'

kill -INT opa
wait opa

# Decision log for allow should have rule_labels with the single matching rule's labels.
jq -s stderr '[.[] | select(.msg == "Decision Log" and .path == "authz/allow")] | .[0].rule_labels == [{"severity": "low"}]'

# Decision log for violations should have one entry per evaluated rule, preserving per-rule grouping.
jq -s stderr '[.[] | select(.msg == "Decision Log" and .path == "authz/violations")] | .[0].rule_labels | sort_by(.category) == [{"severity": "high", "category": "compliance"}, {"severity": "critical", "category": "org-policy"}]'

-- config.yaml --
services:
bundle-server:
url: "http://${HTTP_ADDR}"
bundles:
test:
service: bundle-server
resource: bundle.tar.gz
polling:
min_delay_seconds: 300
max_delay_seconds: 300
decision_logs:
console: true

-- policy/authz.rego --
package authz

default allow := false

# METADATA
# labels:
# severity: low
allow if input.role == "admin"

# METADATA
# labels:
# severity: low
allow if input.role == "editor"

# METADATA
# labels:
# severity: high
# category: compliance
violations contains "admin role is restricted" if input.role == "admin"

# METADATA
# labels:
# severity: critical
# category: org-policy
violations contains "admin not allowed in eng" if {
input.role == "admin"
input.dept == "eng"
}

-- bundles/.gitkeep --
Loading
Loading