Skip to content

Commit ad8c543

Browse files
authored
feat: Add Azure RBAC condition parser (microsoft#496)
* feat: Add Azure RBAC condition parser - declare an `azure-rbac` feature and expose the Azure RBAC module with parser, AST, and YAML-driven tests - extend the shared lexer with RBAC-specific tokens, single-quoted strings, and corrected raw-string spans - verify the parser via comprehensive test cases covering every operator and complex chaining Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> --------- Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
1 parent 49bd3c2 commit ad8c543

21 files changed

Lines changed: 2502 additions & 5 deletions

.github/workflows/pr.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ jobs:
4949
- name: Run tests (Azure Policy)
5050
run: >-
5151
cargo test --frozen --features azure_policy
52+
- name: Run tests (Azure RBAC)
53+
run: cargo test -r --frozen --features azure-rbac

.github/workflows/tests-debug.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,5 @@ jobs:
4242
- name: Run tests (OPA Conformance)
4343
run: >-
4444
cargo test --test opa --frozen --features opa-testutil,serde_json/arbitrary_precision -- $(tr '\n' ' ' < tests/opa.passing)
45+
- name: Run tests (Azure RBAC)
46+
run: cargo test --frozen --features azure-rbac

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ default = ["full-opa", "arc", "rvm"]
2525
arc = ["scientific/arc"]
2626
ast = []
2727
azure_policy = ["dep:jsonschema", "arc", "dashmap"]
28+
azure-rbac = []
2829
base64 = ["dep:data-encoding"]
2930
base64url = ["dep:data-encoding"]
3031
coverage = []
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use alloc::string::String;
5+
use serde::{Deserialize, Serialize};
6+
7+
use crate::value::Value;
8+
9+
/// Principal type
10+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11+
pub enum PrincipalType {
12+
User,
13+
Group,
14+
ServicePrincipal,
15+
ManagedServiceIdentity,
16+
}
17+
18+
/// Evaluation context - what information is available when evaluating RBAC policies
19+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20+
pub struct EvaluationContext {
21+
pub principal: Principal,
22+
pub resource: Resource,
23+
pub request: RequestContext,
24+
pub environment: EnvironmentContext,
25+
pub action: Option<String>,
26+
pub suboperation: Option<String>,
27+
}
28+
29+
/// Principal information (user, group, service principal, etc.)
30+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31+
pub struct Principal {
32+
pub id: String,
33+
pub principal_type: PrincipalType,
34+
pub custom_security_attributes: Value,
35+
}
36+
37+
/// Resource information
38+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39+
pub struct Resource {
40+
pub id: String,
41+
pub resource_type: String,
42+
pub scope: String,
43+
pub attributes: Value,
44+
}
45+
46+
/// Request context information
47+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48+
pub struct RequestContext {
49+
pub action: Option<String>,
50+
pub data_action: Option<String>,
51+
pub attributes: Value,
52+
}
53+
54+
/// Environment context information
55+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56+
pub struct EnvironmentContext {
57+
pub is_private_link: Option<bool>,
58+
pub private_endpoint: Option<String>,
59+
pub subnet: Option<String>,
60+
pub utc_now: Option<String>,
61+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use alloc::boxed::Box;
5+
use alloc::string::String;
6+
use alloc::vec::Vec;
7+
use serde::{Deserialize, Serialize};
8+
9+
use super::literals::{
10+
BooleanLiteral, DateTimeLiteral, NullLiteral, NumberLiteral, StringLiteral, TimeLiteral,
11+
};
12+
use super::operators::{ArrayOperator, ConditionOperator};
13+
use super::references::AttributeReference;
14+
use super::span::EmptySpan;
15+
16+
/// ABAC condition expression
17+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18+
pub struct ConditionExpression {
19+
#[serde(skip)]
20+
pub span: EmptySpan,
21+
pub raw_expression: String,
22+
#[serde(skip_serializing_if = "Option::is_none")]
23+
pub expression: Option<ConditionExpr>,
24+
}
25+
26+
impl ConditionExpression {
27+
pub fn new(span: EmptySpan, expression: String) -> Self {
28+
Self {
29+
span,
30+
raw_expression: expression,
31+
expression: None,
32+
}
33+
}
34+
35+
pub fn with_parsed(span: EmptySpan, raw_expression: String, parsed: ConditionExpr) -> Self {
36+
Self {
37+
span,
38+
raw_expression,
39+
expression: Some(parsed),
40+
}
41+
}
42+
}
43+
44+
/// Condition expression node
45+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46+
#[serde(tag = "type")]
47+
pub enum ConditionExpr {
48+
Logical(LogicalExpression),
49+
Unary(UnaryExpression),
50+
Binary(BinaryExpression),
51+
FunctionCall(FunctionCallExpression),
52+
AttributeReference(AttributeReference),
53+
ArrayExpression(ArrayExpression),
54+
Identifier(IdentifierExpression),
55+
VariableReference(VariableReference),
56+
PropertyAccess(PropertyAccessExpression),
57+
StringLiteral(StringLiteral),
58+
NumberLiteral(NumberLiteral),
59+
BooleanLiteral(BooleanLiteral),
60+
NullLiteral(NullLiteral),
61+
DateTimeLiteral(DateTimeLiteral),
62+
TimeLiteral(TimeLiteral),
63+
SetLiteral(SetLiteral),
64+
ListLiteral(ListLiteral),
65+
}
66+
67+
/// Logical (AND/OR) expression
68+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69+
pub struct LogicalExpression {
70+
#[serde(skip)]
71+
pub span: EmptySpan,
72+
pub operator: LogicalOperator,
73+
pub left: Box<ConditionExpr>,
74+
pub right: Box<ConditionExpr>,
75+
}
76+
77+
/// Logical operator kinds
78+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79+
pub enum LogicalOperator {
80+
And,
81+
Or,
82+
}
83+
84+
/// Unary expression (e.g., NOT)
85+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86+
pub struct UnaryExpression {
87+
#[serde(skip)]
88+
pub span: EmptySpan,
89+
pub operator: UnaryOperator,
90+
pub operand: Box<ConditionExpr>,
91+
}
92+
93+
/// Unary operator kinds
94+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95+
pub enum UnaryOperator {
96+
Not,
97+
Exists,
98+
NotExists,
99+
}
100+
101+
/// Binary expression with an operator and two operands
102+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103+
pub struct BinaryExpression {
104+
#[serde(skip)]
105+
pub span: EmptySpan,
106+
pub operator: ConditionOperator,
107+
pub left: Box<ConditionExpr>,
108+
pub right: Box<ConditionExpr>,
109+
}
110+
111+
/// Function call expression (e.g. ToLower(expr))
112+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113+
pub struct FunctionCallExpression {
114+
#[serde(skip)]
115+
pub span: EmptySpan,
116+
pub function: String,
117+
pub arguments: Vec<ConditionExpr>,
118+
}
119+
120+
/// Array expression with quantifiers (e.g. ANY tag : ...)
121+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122+
pub struct ArrayExpression {
123+
#[serde(skip)]
124+
pub span: EmptySpan,
125+
pub operator: ArrayOperator,
126+
pub array: Box<ConditionExpr>,
127+
#[serde(skip_serializing_if = "Option::is_none")]
128+
pub variable: Option<String>,
129+
pub condition: Box<ConditionExpr>,
130+
}
131+
132+
/// Set literal value (e.g. {'a', 'b'})
133+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134+
pub struct SetLiteral {
135+
#[serde(skip)]
136+
pub span: EmptySpan,
137+
pub elements: Vec<ConditionExpr>,
138+
}
139+
140+
/// List literal value (e.g. ['start', 'end'])
141+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
142+
pub struct ListLiteral {
143+
#[serde(skip)]
144+
pub span: EmptySpan,
145+
pub elements: Vec<ConditionExpr>,
146+
}
147+
148+
/// Identifier expression (unqualified name)
149+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150+
pub struct IdentifierExpression {
151+
#[serde(skip)]
152+
pub span: EmptySpan,
153+
pub name: String,
154+
}
155+
156+
/// Variable reference (e.g. loop variable in ANY clauses)
157+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158+
pub struct VariableReference {
159+
#[serde(skip)]
160+
pub span: EmptySpan,
161+
pub name: String,
162+
}
163+
164+
/// Property access expression (e.g. tag.key)
165+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166+
pub struct PropertyAccessExpression {
167+
#[serde(skip)]
168+
pub span: EmptySpan,
169+
pub object: Box<ConditionExpr>,
170+
pub property: String,
171+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use alloc::string::String;
5+
use serde::{Deserialize, Serialize};
6+
7+
use super::span::EmptySpan;
8+
9+
/// String literal value
10+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11+
pub struct StringLiteral {
12+
#[serde(skip)]
13+
pub span: EmptySpan,
14+
pub value: String,
15+
}
16+
17+
/// Number literal value (keeps raw representation)
18+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19+
pub struct NumberLiteral {
20+
#[serde(skip)]
21+
pub span: EmptySpan,
22+
pub raw: String,
23+
}
24+
25+
/// Boolean literal value
26+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27+
pub struct BooleanLiteral {
28+
#[serde(skip)]
29+
pub span: EmptySpan,
30+
pub value: bool,
31+
}
32+
33+
/// Null literal
34+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35+
pub struct NullLiteral {
36+
#[serde(skip)]
37+
pub span: EmptySpan,
38+
}
39+
40+
/// Date-time literal value (ISO-8601 formatted)
41+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42+
pub struct DateTimeLiteral {
43+
#[serde(skip)]
44+
pub span: EmptySpan,
45+
pub value: String,
46+
#[serde(skip_serializing_if = "Option::is_none")]
47+
pub normalized: Option<String>,
48+
}
49+
50+
/// Time literal value (HH:MM or HH:MM:SS)
51+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52+
pub struct TimeLiteral {
53+
#[serde(skip)]
54+
pub span: EmptySpan,
55+
pub value: String,
56+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
pub mod context;
5+
pub mod expr;
6+
pub mod literals;
7+
pub mod operators;
8+
pub mod references;
9+
pub mod span;
10+
11+
pub use context::*;
12+
pub use expr::*;
13+
pub use literals::*;
14+
pub use operators::*;
15+
pub use references::*;
16+
pub use span::*;

0 commit comments

Comments
 (0)