mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat(azure-policy): add policy rule and policy definition parsers (#660)
Extend the Azure Policy parser to handle complete policyRule and
policyDefinition JSON structures, not just standalone constraints.
Policy rule parser (policy_rule.rs):
- Parse top-level { "if": ..., "then": ... } objects
- Extract effect kind (deny, audit, append, modify, etc.) into typed AST
- Parse "details" structurally when it is an object to pull out
existenceCondition as a first-class Constraint; fall back to opaque
JSON for non-object details (e.g. append array form)
- Detect duplicate/missing keys for "if", "then", "effect", "details"
Policy definition parser (policy_definition.rs):
- Handle both wrapped ARM envelope ({ "properties": { ... } }) and
unwrapped (properties-level keys at top level) forms
- Type-extract displayName, description, mode, metadata, parameters,
and policyRule; everything else goes into extra
- Parse parameter definitions with type, defaultValue, allowedValues,
and metadata; detect duplicate parameter names
- Duplicate key detection throughout
Grammar documentation (docs/azure-policy/azurepolicy.ebnf):
- Add formal EBNF grammar covering policy-rule, then-block,
constraints, conditions, all 19 operators, count expressions,
JSON values, and ARM template expressions
Test harness changes:
- Add parse_level field to YAML test cases: "constraint" (default),
"policy_rule", or "policy_definition"
- Un-skip three parse_errors cases that needed policy_rule-level parsing
- Add policy_rule.yaml with 12 cases covering all 9 effect kinds,
existenceCondition, parameterized effects, complex conditions, and
extra key handling
- Add policy_definition.yaml with wrapped, unwrapped, parameterized,
missing-policyRule, and duplicate-key error cases
This commit is contained in:
committed by
GitHub
parent
687be2850b
commit
8f740e2f6f
@@ -11,7 +11,7 @@ cases:
|
||||
# =========================================================================
|
||||
|
||||
- note: missing_if_key
|
||||
skip: true # Tests policy_rule-level error; needs parse_policy_rule
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"then": { "effect": "deny" }
|
||||
@@ -19,7 +19,7 @@ cases:
|
||||
want_parse_error: true
|
||||
|
||||
- note: missing_then_key
|
||||
skip: true # Tests policy_rule-level error; needs parse_policy_rule
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
@@ -30,7 +30,7 @@ cases:
|
||||
want_parse_error: true
|
||||
|
||||
- note: missing_effect_in_then
|
||||
skip: true # Tests policy_rule-level error; needs parse_policy_rule
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
|
||||
197
tests/azure_policy/parser_tests/cases/policy_definition.yaml
Normal file
197
tests/azure_policy/parser_tests/cases/policy_definition.yaml
Normal file
@@ -0,0 +1,197 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Policy Definition Test Suite
|
||||
# Tests that full policy definition envelopes parse correctly via parse_policy_definition.
|
||||
|
||||
cases:
|
||||
# =========================================================================
|
||||
# Unwrapped form (properties-level keys directly)
|
||||
# =========================================================================
|
||||
|
||||
- note: unwrapped_simple
|
||||
parse_level: policy_definition
|
||||
policy_rule: |
|
||||
{
|
||||
"displayName": "Deny VMs",
|
||||
"description": "Deny creation of VMs",
|
||||
"mode": "All",
|
||||
"policyRule": {
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Compute/virtualMachines"
|
||||
},
|
||||
"then": {
|
||||
"effect": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- note: unwrapped_with_parameters
|
||||
parse_level: policy_definition
|
||||
policy_rule: |
|
||||
{
|
||||
"displayName": "Allowed locations",
|
||||
"mode": "Indexed",
|
||||
"parameters": {
|
||||
"allowedLocations": {
|
||||
"type": "Array",
|
||||
"metadata": {
|
||||
"displayName": "Allowed locations",
|
||||
"description": "The list of allowed locations."
|
||||
}
|
||||
}
|
||||
},
|
||||
"policyRule": {
|
||||
"if": {
|
||||
"not": {
|
||||
"field": "location",
|
||||
"in": "[parameters('allowedLocations')]"
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"effect": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Wrapped form (ARM resource envelope)
|
||||
# =========================================================================
|
||||
|
||||
- note: wrapped_arm_envelope
|
||||
parse_level: policy_definition
|
||||
policy_rule: |
|
||||
{
|
||||
"id": "/providers/Microsoft.Authorization/policyDefinitions/abc",
|
||||
"name": "abc",
|
||||
"type": "Microsoft.Authorization/policyDefinitions",
|
||||
"properties": {
|
||||
"displayName": "Test policy",
|
||||
"policyRule": {
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Storage/storageAccounts"
|
||||
},
|
||||
"then": {
|
||||
"effect": "audit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Missing policyRule (should error)
|
||||
# =========================================================================
|
||||
|
||||
- note: missing_policy_rule
|
||||
parse_level: policy_definition
|
||||
policy_rule: |
|
||||
{
|
||||
"displayName": "No rule here",
|
||||
"mode": "All"
|
||||
}
|
||||
want_parse_error: true
|
||||
|
||||
# =========================================================================
|
||||
# Duplicate keys (should error)
|
||||
# =========================================================================
|
||||
|
||||
- note: wrapped_duplicate_properties_key
|
||||
parse_level: policy_definition
|
||||
policy_rule: |
|
||||
{
|
||||
"id": "/providers/Microsoft.Authorization/policyDefinitions/dup-properties",
|
||||
"name": "dup-properties",
|
||||
"type": "Microsoft.Authorization/policyDefinitions",
|
||||
"properties": {
|
||||
"displayName": "First properties block"
|
||||
},
|
||||
"properties": {
|
||||
"policyRule": {
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Storage/storageAccounts"
|
||||
},
|
||||
"then": {
|
||||
"effect": "audit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
want_parse_error: true
|
||||
|
||||
- note: unwrapped_duplicate_policy_rule
|
||||
parse_level: policy_definition
|
||||
policy_rule: |
|
||||
{
|
||||
"displayName": "Duplicate policyRule",
|
||||
"mode": "All",
|
||||
"policyRule": {
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Compute/virtualMachines"
|
||||
},
|
||||
"then": {
|
||||
"effect": "deny"
|
||||
}
|
||||
},
|
||||
"policyRule": {
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Storage/storageAccounts"
|
||||
},
|
||||
"then": {
|
||||
"effect": "audit"
|
||||
}
|
||||
}
|
||||
}
|
||||
want_parse_error: true
|
||||
|
||||
- note: wrapped_cross_scope_duplicate_key
|
||||
parse_level: policy_definition
|
||||
policy_rule: |
|
||||
{
|
||||
"displayName": "Outer displayName",
|
||||
"properties": {
|
||||
"displayName": "Inner displayName",
|
||||
"policyRule": {
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Compute/virtualMachines"
|
||||
},
|
||||
"then": {
|
||||
"effect": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
want_parse_error: true
|
||||
|
||||
- note: unwrapped_duplicate_parameter_name_case_insensitive
|
||||
parse_level: policy_definition
|
||||
policy_rule: |
|
||||
{
|
||||
"displayName": "Duplicate parameter names by casing",
|
||||
"mode": "Indexed",
|
||||
"parameters": {
|
||||
"allowedLocations": {
|
||||
"type": "Array"
|
||||
},
|
||||
"AllowedLocations": {
|
||||
"type": "Array"
|
||||
}
|
||||
},
|
||||
"policyRule": {
|
||||
"if": {
|
||||
"not": {
|
||||
"field": "location",
|
||||
"in": "[parameters('allowedLocations')]"
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"effect": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
want_parse_error: true
|
||||
225
tests/azure_policy/parser_tests/cases/policy_rule.yaml
Normal file
225
tests/azure_policy/parser_tests/cases/policy_rule.yaml
Normal file
@@ -0,0 +1,225 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Policy Rule Test Suite
|
||||
# Tests that complete policyRule objects (with "if" and "then") parse correctly.
|
||||
|
||||
cases:
|
||||
# =========================================================================
|
||||
# Basic policy rules
|
||||
# =========================================================================
|
||||
|
||||
- note: simple_deny
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Compute/virtualMachines"
|
||||
},
|
||||
"then": {
|
||||
"effect": "deny"
|
||||
}
|
||||
}
|
||||
|
||||
- note: audit_effect
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
"field": "location",
|
||||
"notIn": ["eastus", "westus"]
|
||||
},
|
||||
"then": {
|
||||
"effect": "audit"
|
||||
}
|
||||
}
|
||||
|
||||
- note: disabled_effect
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Storage/storageAccounts"
|
||||
},
|
||||
"then": {
|
||||
"effect": "disabled"
|
||||
}
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Effects with details
|
||||
# =========================================================================
|
||||
|
||||
- note: append_effect
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Storage/storageAccounts"
|
||||
},
|
||||
"then": {
|
||||
"effect": "Append",
|
||||
"details": [
|
||||
{
|
||||
"field": "Microsoft.Storage/storageAccounts/networkAcls.defaultAction",
|
||||
"value": "Deny"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
- note: modify_effect
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Compute/virtualMachines"
|
||||
},
|
||||
"then": {
|
||||
"effect": "Modify",
|
||||
"details": {
|
||||
"roleDefinitionIds": [
|
||||
"/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
|
||||
],
|
||||
"operations": [
|
||||
{
|
||||
"operation": "addOrReplace",
|
||||
"field": "tags.environment",
|
||||
"value": "production"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- note: deny_action_effect
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Sql/servers/databases"
|
||||
},
|
||||
"then": {
|
||||
"effect": "DenyAction",
|
||||
"details": {
|
||||
"actionNames": ["delete"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- note: manual_effect
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Resources/subscriptions"
|
||||
},
|
||||
"then": {
|
||||
"effect": "Manual",
|
||||
"details": {
|
||||
"defaultState": "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- note: deploy_if_not_exists
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Compute/virtualMachines"
|
||||
},
|
||||
"then": {
|
||||
"effect": "DeployIfNotExists",
|
||||
"details": {
|
||||
"type": "Microsoft.Compute/virtualMachines/extensions",
|
||||
"existenceCondition": {
|
||||
"field": "Microsoft.Compute/virtualMachines/extensions/type",
|
||||
"equals": "MicrosoftMonitoringAgent"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- note: audit_if_not_exists
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Sql/servers"
|
||||
},
|
||||
"then": {
|
||||
"effect": "AuditIfNotExists",
|
||||
"details": {
|
||||
"type": "Microsoft.Sql/servers/auditingSettings",
|
||||
"existenceCondition": {
|
||||
"field": "Microsoft.Sql/servers/auditingSettings/state",
|
||||
"equals": "Enabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Parameterized effect
|
||||
# =========================================================================
|
||||
|
||||
- note: parameterized_effect
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Storage/storageAccounts"
|
||||
},
|
||||
"then": {
|
||||
"effect": "[parameters('effect')]"
|
||||
}
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Complex conditions with then
|
||||
# =========================================================================
|
||||
|
||||
- note: allof_condition_with_then
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
"allOf": [
|
||||
{
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Compute/virtualMachines"
|
||||
},
|
||||
{
|
||||
"field": "location",
|
||||
"equals": "eastus"
|
||||
}
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"effect": "deny"
|
||||
}
|
||||
}
|
||||
|
||||
- note: unknown_extra_keys_ignored
|
||||
parse_level: policy_rule
|
||||
policy_rule: |
|
||||
{
|
||||
"if": {
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Compute/virtualMachines"
|
||||
},
|
||||
"then": {
|
||||
"effect": "deny"
|
||||
},
|
||||
"extraKey": "ignored"
|
||||
}
|
||||
@@ -32,6 +32,13 @@ struct TestCase {
|
||||
/// If true, skip this test case.
|
||||
#[serde(default)]
|
||||
pub skip: Option<bool>,
|
||||
|
||||
/// Parsing level: `"constraint"` (default) extracts the `"if"` block and
|
||||
/// calls `parse_constraint`; `"policy_rule"` calls `parse_policy_rule` on
|
||||
/// the full JSON; `"policy_definition"` calls `parse_policy_definition`
|
||||
/// on the full JSON.
|
||||
#[serde(default)]
|
||||
pub parse_level: Option<String>,
|
||||
}
|
||||
|
||||
/// Top-level YAML test file structure.
|
||||
@@ -95,7 +102,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
|
||||
let expects_parse_error = case.want_parse_error == Some(true);
|
||||
|
||||
let policy_rule_json = if let Some(ref rule) = case.policy_rule {
|
||||
let input_json = if let Some(ref rule) = case.policy_rule {
|
||||
rule.clone()
|
||||
} else if let Some(ref rule) = test.policy_rule {
|
||||
rule.clone()
|
||||
@@ -103,15 +110,35 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
panic!("case '{}': must specify 'policy_rule'", case.note);
|
||||
};
|
||||
|
||||
// Extract the "if" constraint JSON. If extraction fails (malformed
|
||||
// JSON or missing "if" key), feed the raw policy_rule to
|
||||
// parse_constraint — it should fail, matching want_parse_error.
|
||||
let constraint_json =
|
||||
extract_if_json(&policy_rule_json).unwrap_or_else(|| policy_rule_json.clone());
|
||||
let parse_level = case.parse_level.as_deref().unwrap_or("constraint");
|
||||
|
||||
let source = Source::from_contents(format!("test:{}", case.note), constraint_json)?;
|
||||
|
||||
let parse_result = parser::parse_constraint(&source).map(|_| ());
|
||||
let parse_result = match parse_level {
|
||||
"policy_rule" => {
|
||||
// Parse the full policy_rule JSON with parse_policy_rule.
|
||||
let source = Source::from_contents(format!("test:{}", case.note), input_json)?;
|
||||
parser::parse_policy_rule(&source).map(|_| ())
|
||||
}
|
||||
"policy_definition" => {
|
||||
// Parse the full policy definition JSON with parse_policy_definition.
|
||||
let source = Source::from_contents(format!("test:{}", case.note), input_json)?;
|
||||
parser::parse_policy_definition(&source).map(|_| ())
|
||||
}
|
||||
"constraint" => {
|
||||
// Extract the "if" constraint JSON. If extraction fails
|
||||
// (malformed JSON or missing "if" key), feed the raw
|
||||
// input to parse_constraint — it should fail,
|
||||
// matching want_parse_error.
|
||||
let constraint_json = match extract_if_json(&input_json) {
|
||||
Some(json) => json,
|
||||
None => input_json,
|
||||
};
|
||||
let source = Source::from_contents(format!("test:{}", case.note), constraint_json)?;
|
||||
parser::parse_constraint(&source).map(|_| ())
|
||||
}
|
||||
other => {
|
||||
panic!("case '{}': unknown parse_level '{}'", case.note, other);
|
||||
}
|
||||
};
|
||||
|
||||
match parse_result {
|
||||
Ok(()) => {
|
||||
|
||||
Reference in New Issue
Block a user