Annotations are key-value pairs that can be attached to various entities for custom metadata and policy decisions. They support inheritance hierarchies that allow general definitions to be overridden by more specific ones.
Annotations provide flexible metadata that:
Annotations enable a powerful pattern: generic policies that are parameterized by entity configuration.
Consider this policy that checks if a principal's environment matches the resource's environment:
package authz
default allow = false
allow {
input.principal.mannotations.environment == input.resource.annotations.environment
}
This policy is completely generic—it doesn't mention any specific environment. The role and resource group definitions parameterize it:
roles:
- mrn: "mrn:iam:role:finance-analyst"
name: finance-analyst
policy: "mrn:iam:policy:environment-match"
annotations:
- name: environment
value: finance # This role grants access to "finance" resources
resource-groups:
- mrn: "mrn:iam:resource-group:finance-data"
name: finance-data
policy: "mrn:iam:policy:resource-access"
annotations:
- name: environment
value: finance # Resources in this group are tagged "finance"
When a principal with the finance-analyst role accesses a resource in the finance-data group:
environment: "finance" annotation flows into input.principal.mannotationsenvironment: "finance" annotation flows into input.resource.annotationsThis pattern is fundamental to the PolicyEngine architecture. Entities like Roles, Groups, Scopes, and Resource Groups serve a dual purpose:
This separation means you can:
Annotations can be defined on multiple entity types:
| Entity | Defined In | Description |
|---|---|---|
| Role | PolicyDomain spec.roles[].annotations | Role-specific metadata |
| Group | PolicyDomain spec.groups[].annotations | Group-specific metadata |
| Scope | PolicyDomain spec.scopes[].annotations | Scope-specific metadata |
| Resource Group | PolicyDomain spec.resource-groups[].annotations | Resource group metadata |
| Resource | PolicyDomain spec.resources[].annotations, or external resource resolution | Resource-specific metadata |
| Principal | JWT claims | Identity-level metadata |
Annotations follow inheritance hierarchies where conflicts are resolved by precedence rules. More specific or contextual definitions override more general ones.
For identity-related annotations (available in input.principal.mannotations), the inheritance order from least to most dominant is:
Example: If the same annotation key department is defined on both a Role and a Group that apply to a request, the Group's value takes precedence. If also defined in the principal's JWT claims, that value wins.
# Role definition
roles:
- mrn: "mrn:iam:role:developer"
annotations:
- name: department
value: engineering # Precedence 1
- name: access_level
value: standard
# Group definition
groups:
- mrn: "mrn:iam:group:platform-team"
annotations:
- name: department
value: platform # Precedence 2 - overrides role
- name: team
value: infrastructure
# Scope definition (if applicable)
scopes:
- mrn: "mrn:iam:scope:elevated"
annotations:
- name: access_level
value: elevated # Precedence 3 - overrides role
# Principal JWT claims
principal:
mannotations:
department: "security" # Precedence 4 - overrides all others
Resulting input.principal.mannotations:
{
"department": "security", // From principal (highest precedence)
"access_level": "elevated", // From scope
"team": "infrastructure" // From group (no conflict)
}
For resource-related annotations (available in input.resource.annotations), the inheritance order from least to most dominant is:
Example: If a resource belongs to a resource group, annotations from the resource group are inherited, but any annotations defined directly on the resource take precedence.
# Resource Group definition
resource-groups:
- mrn: "mrn:iam:resource-group:customer-data"
annotations:
- name: data_classification
value: confidential
- name: retention_days
value: 365
- name: requires_audit
value: true
# Resource with override
resource:
id: "mrn:data:customer:12345"
annotations:
- name: retention_days
value: 730 # Overrides resource group
- name: special_handling
value: true # Additional annotation
Resulting input.resource.annotations:
{
"data_classification": "confidential", // From resource group
"retention_days": "730", // From resource (overrides)
"requires_audit": "true", // From resource group
"special_handling": "true" // From resource (new)
}
:::info[v1alpha4+ Feature]
Merge strategies are available in PolicyDomain schema version v1alpha4 and later (including v1beta1).
:::
When annotation keys conflict across inheritance levels, merge strategies determine how values are combined. The default strategy is deep, which recursively merges arrays and objects while letting higher-priority scalar values win. You can specify different strategies to control this behavior precisely.
Merge strategies are particularly useful for:
| Strategy | Arrays | Objects | Scalars |
|---|---|---|---|
replace | Higher replaces lower | Higher replaces lower | Higher wins |
append | [higher..., lower...] | Shallow merge, higher wins | Higher wins |
prepend | [lower..., higher...] | Shallow merge, lower wins | Lower wins |
deep | [higher..., lower...] | Recursive merge, higher wins | Higher wins |
union | Deduplicated set, higher first | Same as deep | Higher wins |
The default strategy is deep when no strategy is specified.
Add the merge field to an annotation to control how it combines with values from lower-priority sources:
roles:
- mrn: "mrn:iam:role:developer"
annotations:
- name: allowed_regions
value:
- us-west
merge: union # Deduplicate when combined with other sources
groups:
- mrn: "mrn:iam:group:global-team"
annotations:
- name: allowed_regions
value:
- us-east
- eu-west
merge: union # Combined result: ["us-east", "eu-west", "us-west"]
When merging annotations, the strategy is determined by priority:
deep)Use union to collect unique values from all sources:
roles:
- mrn: "mrn:iam:role:developer"
annotations:
- name: tags
value:
- dev
- internal
merge: union
groups:
- mrn: "mrn:iam:group:platform-team"
annotations:
- name: tags
value:
- platform
- internal
merge: union
Result: ["platform", "internal", "dev"] (deduplicated, higher priority first)
Use deep to recursively merge nested objects:
roles:
- mrn: "mrn:iam:role:developer"
annotations:
- name: config
value:
timeouts:
read: 30
write: 60
retries: 3
merge: deep
groups:
- mrn: "mrn:iam:group:premium-users"
annotations:
- name: config
value:
timeouts:
write: 120
priority: high
merge: deep
Result:
{
"timeouts": {"read": 30, "write": 120},
"retries": 3,
"priority": "high"
}
Use append to add lower-priority elements after higher-priority ones:
resource-groups:
- mrn: "mrn:iam:resource-group:base"
annotations:
- name: processing_steps
value:
- validate
- log
merge: append
resources:
- selector: ["mrn:data:sensitive:.*"]
group: "mrn:iam:resource-group:base"
annotations:
- name: processing_steps
value:
- encrypt
- audit
merge: append
Result: ["encrypt", "audit", "validate", "log"] (higher priority first)
Use prepend to add lower-priority elements before higher-priority ones:
# Same as above but with prepend
annotations:
- name: processing_steps
value:
- encrypt
- audit
merge: prepend
Result: ["validate", "log", "encrypt", "audit"] (lower priority first)
Use replace when you want to completely override a lower-priority value:
roles:
- mrn: "mrn:iam:role:standard-user"
annotations:
- name: permissions
value:
- read
- list
groups:
- mrn: "mrn:iam:group:admin"
annotations:
- name: permissions
value:
- read
- write
- delete
- admin
merge: replace # Completely replaces role permissions
Result: ["read", "write", "delete", "admin"] (role permissions ignored)
When conflicting values have incompatible types (e.g., array vs. string), the higher-priority value always wins regardless of the merge strategy:
roles:
- mrn: "mrn:iam:role:basic"
annotations:
- name: access
value:
- read # Array
groups:
- mrn: "mrn:iam:group:special"
annotations:
- name: access
value: full # String (incompatible type)
merge: union # Strategy is ignored for type mismatch
Result: "full" (higher priority wins due to type mismatch)
Define baseline annotations at a general level and override for specific cases:
# All developers get standard access
roles:
- mrn: "mrn:iam:role:developer"
annotations:
- name: max_data_size
value: 1GB
- name: can_export
value: false
# Platform team members can export
groups:
- mrn: "mrn:iam:group:platform-team"
annotations:
- name: can_export
value: true # Override for this group
Apply cumulative security requirements:
resource-groups:
- mrn: "mrn:iam:resource-group:pii"
annotations:
- name: classification
value: PII
- name: encryption_required
value: true
# Specific high-value resource
resource:
annotations:
- name: classification
value: PII-HIGH # More specific classification
- name: two_person_rule
value: true # Additional requirement
Annotations are defined as a list of objects with the following fields:
| Field | Required | Description |
|---|---|---|
name | Yes | The annotation key (string) |
value | Yes | The annotation value (native YAML in v1beta1, JSON-encoded string in v1alpha3/4) |
merge | No | Merge strategy: replace, append, prepend, deep, or union |
In v1beta1, annotation values are written as native YAML—no JSON encoding required:
annotations:
- name: department
value: engineering # String
- name: cost_center
value: 12345 # Number
- name: tags
value: # Array
- production
- critical
- name: metadata
value: # Object
created_by: admin
version: 2
- name: enabled
value: true # Boolean
This is cleaner to read and write than the legacy JSON-encoded format.
In older schema versions, values must be JSON-encoded strings:
:::warning[JSON Encoding Required in v1alpha3/v1alpha4] String values require nested quotes. The outer quotes are YAML string delimiters; the inner escaped quotes are the JSON string value.
| Type | v1alpha4 (JSON-encoded) | v1beta1 (Native) |
|---|---|---|
| String | "\"engineering\"" | engineering |
| Number | "12345" | 12345 |
| Boolean | "true" | true |
| Array | '["read", "write"]' | - read- write |
| Object | '{"region": "us-west"}' | region: us-west |
# v1alpha4 - JSON-encoded (required)
annotations:
- name: "department"
value: "\"engineering\"" # Parsed as JSON string: "engineering"
# v1beta1 - Native YAML (recommended)
annotations:
- name: department
value: engineering # Much cleaner!
:::
After parsing, annotations are available in policies as native values regardless of schema version:
{
"department": "engineering",
"cost_center": 12345,
"tags": ["production", "critical"],
"metadata": {
"created_by": "admin",
"version": 2
},
"enabled": true
}
package authz
default allow = false
# Check department
allow {
input.principal.mannotations.department == "engineering"
}
# Check resource tag
allow {
input.resource.annotations.environment == "production"
}
package authz
default allow = false
# Principal must have required capability
allow {
"admin" in input.principal.mannotations.capabilities
}
# Resource must have a matching tag
allow {
some tag in input.resource.annotations.tags
tag in input.principal.mannotations.allowed_tags
}
package authz
default allow = false
# Check nested value
allow {
input.resource.annotations.metadata.level >= 2
}
package authz
default allow = false
# Principal and resource department must match
allow {
input.principal.mannotations.department == input.resource.annotations.department
}
# Principal must have access to the resource's region
allow {
input.resource.annotations.region in input.principal.mannotations.allowed_regions
}
spec:
roles:
- mrn: "mrn:iam:role:regional-admin"
name: regional-admin
annotations:
- name: region
value: us-west
- name: permissions
value:
- read
- write
- admin
policy: "mrn:iam:policy:regional-access"
spec:
groups:
- mrn: "mrn:iam:group:finance"
name: finance
annotations:
- name: department
value: finance
- name: cost_center
value: 12345
roles:
- "mrn:iam:role:finance-user"
spec:
resource-groups:
- mrn: "mrn:iam:resource-group:pii-data"
name: pii-data
annotations:
- name: data_classification
value: PII
- name: retention_days
value: 365
- name: requires_audit
value: true
policy: "mrn:iam:policy:pii-access"
package authz
default allow = false
# Same department access
allow {
input.principal.mannotations.department == input.resource.annotations.department
}
package authz
default allow = false
# Principal's region must include the resource's region
allow {
input.resource.annotations.region in input.principal.mannotations.allowed_regions
}
package authz
default allow = false
# Check if access hasn't expired
allow {
expires := time.parse_rfc3339_ns(input.resource.annotations.access_expires)
expires > time.now_ns()
}
package authz
default allow = false
# Check feature flag
allow {
input.principal.mannotations.beta_features == true
startswith(input.operation, "beta:")
}
union for accumulating unique values, deep for configuration objects, and replace when you need complete override behaviorCan you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |