Skip to content

Commit 5c7e99f

Browse files
committed
labels: aggregate from package/subpackage/document scope, update CHANGELOG
Small follow-up to open-policy-agent#8613. I think carrying the labels from upper scopes along makes this more powerful. Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
1 parent 84cbb8e commit 5c7e99f

5 files changed

Lines changed: 158 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,18 @@ Rule annotations now support a `labels` field. Labels from all successfully eval
1111
rules are collected and included in each decision log entry as a top-level `rule_labels`
1212
array. Each element preserves the label map from one evaluated rule.
1313

14+
Labels can be defined at package, document, rule, or subpackages scope. Package-scoped
15+
labels are inherited by all rules in the package. Subpackages-scoped labels apply to all
16+
rules in descendant packages. Document-scoped labels apply to all rules with the same
17+
name. All scopes are aggregated and deduplicated.
18+
1419
```rego
20+
# METADATA
21+
# scope: package
22+
# labels:
23+
# service: authz
24+
package myapp
25+
1526
# METADATA
1627
# labels:
1728
# severity: low
@@ -22,10 +33,10 @@ allow if input.role == "admin"
2233
The resulting decision log entry will contain:
2334

2435
```json
25-
{"rule_labels": [{"severity": "low", "team": "platform"}]}
36+
{"rule_labels": [{"service": "authz"}, {"severity": "low", "team": "platform"}]}
2637
```
2738

28-
The runtime now processes metadata annotations by default.
39+
Both the runtime and the Go SDK now process metadata annotations by default.
2940

3041
## 1.16.1
3142

docs/docs/policy-language.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2668,10 +2668,11 @@ message := "welcome!" if allow
26682668
### Metadata `labels`
26692669

26702670
The `labels` annotation is a map of arbitrary key-value pairs attached to a
2671-
rule (or document). When rules with `labels` are successfully evaluated, their
2672-
label sets are automatically recorded in decision log events under the
2673-
`rule_labels` field. Labels from document-scoped and rule-scoped annotations
2674-
are both collected.
2671+
rule (or document, package, or subpackages scope). When rules with `labels` are
2672+
successfully evaluated, their label sets are automatically recorded in decision
2673+
log events under the `rule_labels` field. Labels from subpackages-scoped,
2674+
package-scoped, document-scoped, and rule-scoped annotations are all collected
2675+
and aggregated.
26752676

26762677
```rego
26772678
# METADATA

v1/ast/annotations.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,20 @@ func attachRuleAnnotations(mod *Module) {
544544
cpy[i] = a.Copy(a.node)
545545
}
546546

547+
// Collect package-scope labels to propagate to all rules.
548+
var pkgLabels map[string]any
549+
for _, a := range cpy {
550+
if a.Scope == annotationScopePackage && len(a.Labels) > 0 {
551+
pkgLabels = a.Labels
552+
break
553+
}
554+
}
555+
547556
for _, rule := range mod.Rules {
557+
if pkgLabels != nil {
558+
rule.Annotations = append(rule.Annotations, &Annotations{Labels: pkgLabels})
559+
}
560+
548561
var j int
549562
var found bool
550563
for i, a := range cpy {

v1/ast/compile.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,6 +1852,30 @@ func (c *Compiler) setAnnotationSet() {
18521852
c.err(err)
18531853
}
18541854
c.annotationSet = as
1855+
1856+
// Propagate subpackages-scoped labels to rules in descendant packages.
1857+
for _, mod := range c.Modules {
1858+
subPkgAnnots := as.GetSubpackagesScope(mod.Package.Path)
1859+
if len(subPkgAnnots) == 0 {
1860+
continue
1861+
}
1862+
var subPkgLabels []map[string]any
1863+
for _, a := range subPkgAnnots {
1864+
if len(a.Labels) > 0 {
1865+
subPkgLabels = append(subPkgLabels, a.Labels)
1866+
}
1867+
}
1868+
if len(subPkgLabels) == 0 {
1869+
continue
1870+
}
1871+
for _, rule := range mod.Rules {
1872+
prepend := make([]*Annotations, len(subPkgLabels))
1873+
for i, labels := range subPkgLabels {
1874+
prepend[i] = &Annotations{Labels: labels}
1875+
}
1876+
rule.Annotations = append(prepend, rule.Annotations...)
1877+
}
1878+
}
18551879
}
18561880

18571881
// checkTypes runs the type checker on all rules. The type checker builds a

v1/topdown/evaluated_test.go

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,12 @@ func TestEvaluatedRuleTracker(t *testing.T) {
9595

9696
func TestEvaluatedRuleLabelsScopes(t *testing.T) {
9797
tests := []struct {
98-
note string
99-
module string
100-
query string
101-
input string
102-
exp []map[string]any
98+
note string
99+
module string
100+
modules map[string]string
101+
query string
102+
input string
103+
exp []map[string]any
103104
}{
104105
{
105106
note: "rule scope labels",
@@ -190,13 +191,108 @@ allow if input.role == "viewer"
190191
{"id": "allow-admin"},
191192
},
192193
},
194+
{
195+
note: "package scope labels inherited by rules",
196+
module: `# METADATA
197+
# scope: package
198+
# labels:
199+
# service: auth
200+
package test
201+
202+
# METADATA
203+
# labels:
204+
# severity: high
205+
allow if input.role == "admin"
206+
`,
207+
query: "data.test.allow",
208+
exp: []map[string]any{
209+
{"service": "auth"},
210+
{"severity": "high"},
211+
},
212+
},
213+
{
214+
note: "package and document and rule scope all combine",
215+
module: `# METADATA
216+
# scope: package
217+
# labels:
218+
# service: auth
219+
package test
220+
221+
# METADATA
222+
# scope: document
223+
# labels:
224+
# component: authz
225+
226+
# METADATA
227+
# labels:
228+
# severity: high
229+
allow if input.role == "admin"
230+
`,
231+
query: "data.test.allow",
232+
exp: []map[string]any{
233+
{"service": "auth"},
234+
{"component": "authz"},
235+
{"severity": "high"},
236+
},
237+
},
238+
{
239+
note: "both rules fire, both severities collected",
240+
module: `package test
241+
242+
# METADATA
243+
# labels:
244+
# severity: high
245+
reasons contains "admin" if "admin" in input.roles
246+
247+
# METADATA
248+
# labels:
249+
# severity: low
250+
reasons contains "viewer" if "viewer" in input.roles
251+
`,
252+
query: "data.test.reasons",
253+
input: `{"roles": ["admin", "viewer"]}`,
254+
exp: []map[string]any{
255+
{"severity": "high"},
256+
{"severity": "low"},
257+
},
258+
},
259+
{
260+
note: "subpackages scope labels inherited by rules in child packages",
261+
modules: map[string]string{
262+
"parent": `# METADATA
263+
# scope: subpackages
264+
# labels:
265+
# org: acme
266+
package test
267+
`,
268+
"child": `package test.authz
269+
270+
# METADATA
271+
# labels:
272+
# severity: high
273+
allow if input.role == "admin"
274+
`,
275+
},
276+
query: "data.test.authz.allow",
277+
exp: []map[string]any{
278+
{"org": "acme"},
279+
{"severity": "high"},
280+
},
281+
},
193282
}
194283

195284
for _, tc := range tests {
196285
t.Run(tc.note, func(t *testing.T) {
197-
mod := ast.MustParseModuleWithOpts(tc.module, ast.ParserOptions{ProcessAnnotation: true})
286+
modules := make(map[string]*ast.Module)
287+
if tc.modules != nil {
288+
for name, src := range tc.modules {
289+
modules[name] = ast.MustParseModuleWithOpts(src, ast.ParserOptions{ProcessAnnotation: true})
290+
}
291+
} else {
292+
modules["test"] = ast.MustParseModuleWithOpts(tc.module, ast.ParserOptions{ProcessAnnotation: true})
293+
}
198294
c := ast.NewCompiler()
199-
c.Compile(map[string]*ast.Module{"test": mod})
295+
c.Compile(modules)
200296
if c.Failed() {
201297
t.Fatal(c.Errors)
202298
}

0 commit comments

Comments
 (0)