mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat: Add Azure RBAC condition parser (#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>
This commit is contained in:
committed by
GitHub
parent
49bd3c22f3
commit
ad8c543fb5
2
.github/workflows/pr.yml
vendored
2
.github/workflows/pr.yml
vendored
@@ -49,3 +49,5 @@ jobs:
|
||||
- name: Run tests (Azure Policy)
|
||||
run: >-
|
||||
cargo test --frozen --features azure_policy
|
||||
- name: Run tests (Azure RBAC)
|
||||
run: cargo test -r --frozen --features azure-rbac
|
||||
|
||||
2
.github/workflows/tests-debug.yml
vendored
2
.github/workflows/tests-debug.yml
vendored
@@ -42,3 +42,5 @@ jobs:
|
||||
- name: Run tests (OPA Conformance)
|
||||
run: >-
|
||||
cargo test --test opa --frozen --features opa-testutil,serde_json/arbitrary_precision -- $(tr '\n' ' ' < tests/opa.passing)
|
||||
- name: Run tests (Azure RBAC)
|
||||
run: cargo test --frozen --features azure-rbac
|
||||
|
||||
@@ -25,6 +25,7 @@ default = ["full-opa", "arc", "rvm"]
|
||||
arc = ["scientific/arc"]
|
||||
ast = []
|
||||
azure_policy = ["dep:jsonschema", "arc", "dashmap"]
|
||||
azure-rbac = []
|
||||
base64 = ["dep:data-encoding"]
|
||||
base64url = ["dep:data-encoding"]
|
||||
coverage = []
|
||||
|
||||
61
src/languages/azure_rbac/ast/context.rs
Normal file
61
src/languages/azure_rbac/ast/context.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::string::String;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::value::Value;
|
||||
|
||||
/// Principal type
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum PrincipalType {
|
||||
User,
|
||||
Group,
|
||||
ServicePrincipal,
|
||||
ManagedServiceIdentity,
|
||||
}
|
||||
|
||||
/// Evaluation context - what information is available when evaluating RBAC policies
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EvaluationContext {
|
||||
pub principal: Principal,
|
||||
pub resource: Resource,
|
||||
pub request: RequestContext,
|
||||
pub environment: EnvironmentContext,
|
||||
pub action: Option<String>,
|
||||
pub suboperation: Option<String>,
|
||||
}
|
||||
|
||||
/// Principal information (user, group, service principal, etc.)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Principal {
|
||||
pub id: String,
|
||||
pub principal_type: PrincipalType,
|
||||
pub custom_security_attributes: Value,
|
||||
}
|
||||
|
||||
/// Resource information
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Resource {
|
||||
pub id: String,
|
||||
pub resource_type: String,
|
||||
pub scope: String,
|
||||
pub attributes: Value,
|
||||
}
|
||||
|
||||
/// Request context information
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RequestContext {
|
||||
pub action: Option<String>,
|
||||
pub data_action: Option<String>,
|
||||
pub attributes: Value,
|
||||
}
|
||||
|
||||
/// Environment context information
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EnvironmentContext {
|
||||
pub is_private_link: Option<bool>,
|
||||
pub private_endpoint: Option<String>,
|
||||
pub subnet: Option<String>,
|
||||
pub utc_now: Option<String>,
|
||||
}
|
||||
171
src/languages/azure_rbac/ast/expr.rs
Normal file
171
src/languages/azure_rbac/ast/expr.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::literals::{
|
||||
BooleanLiteral, DateTimeLiteral, NullLiteral, NumberLiteral, StringLiteral, TimeLiteral,
|
||||
};
|
||||
use super::operators::{ArrayOperator, ConditionOperator};
|
||||
use super::references::AttributeReference;
|
||||
use super::span::EmptySpan;
|
||||
|
||||
/// ABAC condition expression
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ConditionExpression {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub raw_expression: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expression: Option<ConditionExpr>,
|
||||
}
|
||||
|
||||
impl ConditionExpression {
|
||||
pub fn new(span: EmptySpan, expression: String) -> Self {
|
||||
Self {
|
||||
span,
|
||||
raw_expression: expression,
|
||||
expression: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_parsed(span: EmptySpan, raw_expression: String, parsed: ConditionExpr) -> Self {
|
||||
Self {
|
||||
span,
|
||||
raw_expression,
|
||||
expression: Some(parsed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Condition expression node
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ConditionExpr {
|
||||
Logical(LogicalExpression),
|
||||
Unary(UnaryExpression),
|
||||
Binary(BinaryExpression),
|
||||
FunctionCall(FunctionCallExpression),
|
||||
AttributeReference(AttributeReference),
|
||||
ArrayExpression(ArrayExpression),
|
||||
Identifier(IdentifierExpression),
|
||||
VariableReference(VariableReference),
|
||||
PropertyAccess(PropertyAccessExpression),
|
||||
StringLiteral(StringLiteral),
|
||||
NumberLiteral(NumberLiteral),
|
||||
BooleanLiteral(BooleanLiteral),
|
||||
NullLiteral(NullLiteral),
|
||||
DateTimeLiteral(DateTimeLiteral),
|
||||
TimeLiteral(TimeLiteral),
|
||||
SetLiteral(SetLiteral),
|
||||
ListLiteral(ListLiteral),
|
||||
}
|
||||
|
||||
/// Logical (AND/OR) expression
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct LogicalExpression {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub operator: LogicalOperator,
|
||||
pub left: Box<ConditionExpr>,
|
||||
pub right: Box<ConditionExpr>,
|
||||
}
|
||||
|
||||
/// Logical operator kinds
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum LogicalOperator {
|
||||
And,
|
||||
Or,
|
||||
}
|
||||
|
||||
/// Unary expression (e.g., NOT)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct UnaryExpression {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub operator: UnaryOperator,
|
||||
pub operand: Box<ConditionExpr>,
|
||||
}
|
||||
|
||||
/// Unary operator kinds
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum UnaryOperator {
|
||||
Not,
|
||||
Exists,
|
||||
NotExists,
|
||||
}
|
||||
|
||||
/// Binary expression with an operator and two operands
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BinaryExpression {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub operator: ConditionOperator,
|
||||
pub left: Box<ConditionExpr>,
|
||||
pub right: Box<ConditionExpr>,
|
||||
}
|
||||
|
||||
/// Function call expression (e.g. ToLower(expr))
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FunctionCallExpression {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub function: String,
|
||||
pub arguments: Vec<ConditionExpr>,
|
||||
}
|
||||
|
||||
/// Array expression with quantifiers (e.g. ANY tag : ...)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ArrayExpression {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub operator: ArrayOperator,
|
||||
pub array: Box<ConditionExpr>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub variable: Option<String>,
|
||||
pub condition: Box<ConditionExpr>,
|
||||
}
|
||||
|
||||
/// Set literal value (e.g. {'a', 'b'})
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SetLiteral {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub elements: Vec<ConditionExpr>,
|
||||
}
|
||||
|
||||
/// List literal value (e.g. ['start', 'end'])
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ListLiteral {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub elements: Vec<ConditionExpr>,
|
||||
}
|
||||
|
||||
/// Identifier expression (unqualified name)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct IdentifierExpression {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Variable reference (e.g. loop variable in ANY clauses)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct VariableReference {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Property access expression (e.g. tag.key)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PropertyAccessExpression {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub object: Box<ConditionExpr>,
|
||||
pub property: String,
|
||||
}
|
||||
56
src/languages/azure_rbac/ast/literals.rs
Normal file
56
src/languages/azure_rbac/ast/literals.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::string::String;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::span::EmptySpan;
|
||||
|
||||
/// String literal value
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StringLiteral {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// Number literal value (keeps raw representation)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NumberLiteral {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub raw: String,
|
||||
}
|
||||
|
||||
/// Boolean literal value
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BooleanLiteral {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub value: bool,
|
||||
}
|
||||
|
||||
/// Null literal
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NullLiteral {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
}
|
||||
|
||||
/// Date-time literal value (ISO-8601 formatted)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DateTimeLiteral {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub value: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub normalized: Option<String>,
|
||||
}
|
||||
|
||||
/// Time literal value (HH:MM or HH:MM:SS)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct TimeLiteral {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub value: String,
|
||||
}
|
||||
16
src/languages/azure_rbac/ast/mod.rs
Normal file
16
src/languages/azure_rbac/ast/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
pub mod context;
|
||||
pub mod expr;
|
||||
pub mod literals;
|
||||
pub mod operators;
|
||||
pub mod references;
|
||||
pub mod span;
|
||||
|
||||
pub use context::*;
|
||||
pub use expr::*;
|
||||
pub use literals::*;
|
||||
pub use operators::*;
|
||||
pub use references::*;
|
||||
pub use span::*;
|
||||
207
src/languages/azure_rbac/ast/operators.rs
Normal file
207
src/languages/azure_rbac/ast/operators.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::string::String;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Array operator descriptor (e.g. ANY, ForAnyOfAnyValues:StringEquals)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ArrayOperator {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub modifier: Option<String>,
|
||||
}
|
||||
|
||||
/// Condition operator for Azure RBAC expressions
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ConditionOperator {
|
||||
// String operators
|
||||
StringEquals,
|
||||
StringNotEquals,
|
||||
StringEqualsIgnoreCase,
|
||||
StringNotEqualsIgnoreCase,
|
||||
StringLike,
|
||||
StringNotLike,
|
||||
StringStartsWith,
|
||||
StringNotStartsWith,
|
||||
StringEndsWith,
|
||||
StringNotEndsWith,
|
||||
StringContains,
|
||||
StringNotContains,
|
||||
StringMatches,
|
||||
StringNotMatches,
|
||||
|
||||
// Numeric operators
|
||||
NumericEquals,
|
||||
NumericNotEquals,
|
||||
NumericLessThan,
|
||||
NumericLessThanEquals,
|
||||
NumericGreaterThan,
|
||||
NumericGreaterThanEquals,
|
||||
NumericInRange,
|
||||
|
||||
// Boolean operators
|
||||
BoolEquals,
|
||||
BoolNotEquals,
|
||||
|
||||
// DateTime operators
|
||||
DateTimeEquals,
|
||||
DateTimeNotEquals,
|
||||
DateTimeGreaterThan,
|
||||
DateTimeGreaterThanEquals,
|
||||
DateTimeLessThan,
|
||||
DateTimeLessThanEquals,
|
||||
|
||||
// Time operators
|
||||
TimeOfDayEquals,
|
||||
TimeOfDayNotEquals,
|
||||
TimeOfDayGreaterThan,
|
||||
TimeOfDayGreaterThanEquals,
|
||||
TimeOfDayLessThan,
|
||||
TimeOfDayLessThanEquals,
|
||||
TimeOfDayInRange,
|
||||
|
||||
// GUID operators
|
||||
GuidEquals,
|
||||
GuidNotEquals,
|
||||
|
||||
// IP operators
|
||||
IpMatch,
|
||||
IpNotMatch,
|
||||
IpInRange,
|
||||
|
||||
// List operators
|
||||
ListContains,
|
||||
ListNotContains,
|
||||
|
||||
// Array quantifier operators
|
||||
ForAnyOfAnyValues,
|
||||
ForAllOfAnyValues,
|
||||
ForAnyOfAllValues,
|
||||
ForAllOfAllValues,
|
||||
|
||||
// Action operators
|
||||
ActionMatches,
|
||||
SubOperationMatches,
|
||||
}
|
||||
|
||||
impl ConditionOperator {
|
||||
/// Parse a condition operator from string identifier
|
||||
pub fn from_name(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"StringEquals" => Some(Self::StringEquals),
|
||||
"StringNotEquals" => Some(Self::StringNotEquals),
|
||||
"StringEqualsIgnoreCase" => Some(Self::StringEqualsIgnoreCase),
|
||||
"StringNotEqualsIgnoreCase" => Some(Self::StringNotEqualsIgnoreCase),
|
||||
"StringLike" => Some(Self::StringLike),
|
||||
"StringNotLike" => Some(Self::StringNotLike),
|
||||
"StringStartsWith" => Some(Self::StringStartsWith),
|
||||
"StringNotStartsWith" => Some(Self::StringNotStartsWith),
|
||||
"StringEndsWith" => Some(Self::StringEndsWith),
|
||||
"StringNotEndsWith" => Some(Self::StringNotEndsWith),
|
||||
"StringContains" => Some(Self::StringContains),
|
||||
"StringNotContains" => Some(Self::StringNotContains),
|
||||
"StringMatches" => Some(Self::StringMatches),
|
||||
"StringNotMatches" => Some(Self::StringNotMatches),
|
||||
"NumericEquals" => Some(Self::NumericEquals),
|
||||
"NumericNotEquals" => Some(Self::NumericNotEquals),
|
||||
"NumericLessThan" => Some(Self::NumericLessThan),
|
||||
"NumericLessThanEquals" => Some(Self::NumericLessThanEquals),
|
||||
"NumericGreaterThan" => Some(Self::NumericGreaterThan),
|
||||
"NumericGreaterThanEquals" => Some(Self::NumericGreaterThanEquals),
|
||||
"NumericInRange" => Some(Self::NumericInRange),
|
||||
"BoolEquals" => Some(Self::BoolEquals),
|
||||
"BoolNotEquals" => Some(Self::BoolNotEquals),
|
||||
"DateTimeEquals" => Some(Self::DateTimeEquals),
|
||||
"DateTimeNotEquals" => Some(Self::DateTimeNotEquals),
|
||||
"DateTimeGreaterThan" => Some(Self::DateTimeGreaterThan),
|
||||
"DateTimeGreaterThanEquals" => Some(Self::DateTimeGreaterThanEquals),
|
||||
"DateTimeLessThan" => Some(Self::DateTimeLessThan),
|
||||
"DateTimeLessThanEquals" => Some(Self::DateTimeLessThanEquals),
|
||||
"TimeOfDayEquals" => Some(Self::TimeOfDayEquals),
|
||||
"TimeOfDayNotEquals" => Some(Self::TimeOfDayNotEquals),
|
||||
"TimeOfDayGreaterThan" => Some(Self::TimeOfDayGreaterThan),
|
||||
"TimeOfDayGreaterThanEquals" => Some(Self::TimeOfDayGreaterThanEquals),
|
||||
"TimeOfDayLessThan" => Some(Self::TimeOfDayLessThan),
|
||||
"TimeOfDayLessThanEquals" => Some(Self::TimeOfDayLessThanEquals),
|
||||
"TimeOfDayInRange" => Some(Self::TimeOfDayInRange),
|
||||
"GuidEquals" => Some(Self::GuidEquals),
|
||||
"GuidNotEquals" => Some(Self::GuidNotEquals),
|
||||
"IpMatch" => Some(Self::IpMatch),
|
||||
"IpNotMatch" => Some(Self::IpNotMatch),
|
||||
"IpInRange" => Some(Self::IpInRange),
|
||||
"ListContains" => Some(Self::ListContains),
|
||||
"ListNotContains" => Some(Self::ListNotContains),
|
||||
"ForAnyOfAnyValues" => Some(Self::ForAnyOfAnyValues),
|
||||
"ForAllOfAnyValues" => Some(Self::ForAllOfAnyValues),
|
||||
"ForAnyOfAllValues" => Some(Self::ForAnyOfAllValues),
|
||||
"ForAllOfAllValues" => Some(Self::ForAllOfAllValues),
|
||||
"ActionMatches" => Some(Self::ActionMatches),
|
||||
"SubOperationMatches" => Some(Self::SubOperationMatches),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert condition operator to string
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::StringEquals => "StringEquals",
|
||||
Self::StringNotEquals => "StringNotEquals",
|
||||
Self::StringEqualsIgnoreCase => "StringEqualsIgnoreCase",
|
||||
Self::StringNotEqualsIgnoreCase => "StringNotEqualsIgnoreCase",
|
||||
Self::StringLike => "StringLike",
|
||||
Self::StringNotLike => "StringNotLike",
|
||||
Self::StringStartsWith => "StringStartsWith",
|
||||
Self::StringNotStartsWith => "StringNotStartsWith",
|
||||
Self::StringEndsWith => "StringEndsWith",
|
||||
Self::StringNotEndsWith => "StringNotEndsWith",
|
||||
Self::StringContains => "StringContains",
|
||||
Self::StringNotContains => "StringNotContains",
|
||||
Self::StringMatches => "StringMatches",
|
||||
Self::StringNotMatches => "StringNotMatches",
|
||||
Self::NumericEquals => "NumericEquals",
|
||||
Self::NumericNotEquals => "NumericNotEquals",
|
||||
Self::NumericLessThan => "NumericLessThan",
|
||||
Self::NumericLessThanEquals => "NumericLessThanEquals",
|
||||
Self::NumericGreaterThan => "NumericGreaterThan",
|
||||
Self::NumericGreaterThanEquals => "NumericGreaterThanEquals",
|
||||
Self::NumericInRange => "NumericInRange",
|
||||
Self::BoolEquals => "BoolEquals",
|
||||
Self::BoolNotEquals => "BoolNotEquals",
|
||||
Self::DateTimeEquals => "DateTimeEquals",
|
||||
Self::DateTimeNotEquals => "DateTimeNotEquals",
|
||||
Self::DateTimeGreaterThan => "DateTimeGreaterThan",
|
||||
Self::DateTimeGreaterThanEquals => "DateTimeGreaterThanEquals",
|
||||
Self::DateTimeLessThan => "DateTimeLessThan",
|
||||
Self::DateTimeLessThanEquals => "DateTimeLessThanEquals",
|
||||
Self::TimeOfDayEquals => "TimeOfDayEquals",
|
||||
Self::TimeOfDayNotEquals => "TimeOfDayNotEquals",
|
||||
Self::TimeOfDayGreaterThan => "TimeOfDayGreaterThan",
|
||||
Self::TimeOfDayGreaterThanEquals => "TimeOfDayGreaterThanEquals",
|
||||
Self::TimeOfDayLessThan => "TimeOfDayLessThan",
|
||||
Self::TimeOfDayLessThanEquals => "TimeOfDayLessThanEquals",
|
||||
Self::TimeOfDayInRange => "TimeOfDayInRange",
|
||||
Self::GuidEquals => "GuidEquals",
|
||||
Self::GuidNotEquals => "GuidNotEquals",
|
||||
Self::IpMatch => "IpMatch",
|
||||
Self::IpNotMatch => "IpNotMatch",
|
||||
Self::IpInRange => "IpInRange",
|
||||
Self::ListContains => "ListContains",
|
||||
Self::ListNotContains => "ListNotContains",
|
||||
Self::ForAnyOfAnyValues => "ForAnyOfAnyValues",
|
||||
Self::ForAllOfAnyValues => "ForAllOfAnyValues",
|
||||
Self::ForAnyOfAllValues => "ForAnyOfAllValues",
|
||||
Self::ForAllOfAllValues => "ForAllOfAllValues",
|
||||
Self::ActionMatches => "ActionMatches",
|
||||
Self::SubOperationMatches => "SubOperationMatches",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::str::FromStr for ConditionOperator {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::from_name(s).ok_or(())
|
||||
}
|
||||
}
|
||||
38
src/languages/azure_rbac/ast/references.rs
Normal file
38
src/languages/azure_rbac/ast/references.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::span::EmptySpan;
|
||||
|
||||
/// Attribute reference like @Request[namespace:attribute]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AttributeReference {
|
||||
#[serde(skip)]
|
||||
pub span: EmptySpan,
|
||||
pub source: AttributeSource,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub namespace: Option<String>,
|
||||
pub attribute: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub path: Vec<AttributePathSegment>,
|
||||
}
|
||||
|
||||
/// Source of an attribute reference
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum AttributeSource {
|
||||
Request,
|
||||
Resource,
|
||||
Principal,
|
||||
Environment,
|
||||
Context,
|
||||
}
|
||||
|
||||
/// A segment of an attribute path (e.g. metadata, 0, category)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum AttributePathSegment {
|
||||
Key(String),
|
||||
Index(usize),
|
||||
}
|
||||
8
src/languages/azure_rbac/ast/span.rs
Normal file
8
src/languages/azure_rbac/ast/span.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Empty span placeholder since we don't need spans for RBAC
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct EmptySpan;
|
||||
8
src/languages/azure_rbac/mod.rs
Normal file
8
src/languages/azure_rbac/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
pub mod ast;
|
||||
pub mod parser;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
200
src/languages/azure_rbac/parser/condition_parser.rs
Normal file
200
src/languages/azure_rbac/parser/condition_parser.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::format;
|
||||
use alloc::string::ToString;
|
||||
|
||||
use crate::languages::azure_rbac::ast::{
|
||||
BinaryExpression, ConditionExpr, ConditionExpression, ConditionOperator, EmptySpan,
|
||||
LogicalExpression, LogicalOperator, UnaryExpression, UnaryOperator,
|
||||
};
|
||||
use crate::lexer::{AzureRbacTokenKind, Lexer, Source, Token, TokenKind};
|
||||
|
||||
use super::error::ConditionParseError;
|
||||
|
||||
/// Parse a condition expression string into AST
|
||||
pub fn parse_condition_expression(
|
||||
condition_str: &str,
|
||||
) -> Result<ConditionExpression, ConditionParseError> {
|
||||
if condition_str.trim().is_empty() {
|
||||
return Err(ConditionParseError::UnsupportedCondition(
|
||||
"Empty condition expression".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let source = Source::from_contents("condition".to_string(), condition_str.to_string())
|
||||
.map_err(|e| ConditionParseError::InvalidExpression(e.to_string()))?;
|
||||
let parsed_expr = ConditionParser::parse(&source)?;
|
||||
|
||||
Ok(ConditionExpression::with_parsed(
|
||||
EmptySpan,
|
||||
condition_str.to_string(),
|
||||
parsed_expr,
|
||||
))
|
||||
}
|
||||
|
||||
/// Condition expression parser
|
||||
pub struct ConditionParser<'source> {
|
||||
pub(super) lexer: Lexer<'source>,
|
||||
pub(super) current: Token,
|
||||
}
|
||||
|
||||
impl<'source> ConditionParser<'source> {
|
||||
/// Parse a condition expression from source
|
||||
pub fn parse(source: &'source Source) -> Result<ConditionExpr, ConditionParseError> {
|
||||
let mut lexer = Lexer::new(source);
|
||||
lexer.set_enable_rbac_tokens(true);
|
||||
lexer.set_allow_single_quoted_strings(true);
|
||||
|
||||
let current = lexer
|
||||
.next_token()
|
||||
.map_err(|e| ConditionParseError::InvalidExpression(e.to_string()))?;
|
||||
|
||||
let mut parser = Self { lexer, current };
|
||||
let expression = parser.parse_or_expression()?;
|
||||
|
||||
if parser.current.0 != TokenKind::Eof {
|
||||
return Err(ConditionParseError::UnsupportedCondition(format!(
|
||||
"Unexpected token after expression: {:?} '{}'",
|
||||
parser.current.0,
|
||||
parser.current_text()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(expression)
|
||||
}
|
||||
|
||||
/// Get current token text
|
||||
pub(super) fn current_text(&self) -> &str {
|
||||
self.current.1.text()
|
||||
}
|
||||
|
||||
/// Advance to next token
|
||||
pub(super) fn advance(&mut self) -> Result<(), ConditionParseError> {
|
||||
self.current = self
|
||||
.lexer
|
||||
.next_token()
|
||||
.map_err(|e| ConditionParseError::InvalidExpression(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if current token matches expected
|
||||
pub(super) fn expect(&mut self, expected: TokenKind) -> Result<(), ConditionParseError> {
|
||||
if self.current.0 != expected {
|
||||
return Err(ConditionParseError::UnsupportedCondition(format!(
|
||||
"Expected {:?}, found {:?} at {}",
|
||||
expected,
|
||||
self.current.0,
|
||||
self.current_text()
|
||||
)));
|
||||
}
|
||||
self.advance()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse OR expression (lowest precedence)
|
||||
pub(super) fn parse_or_expression(&mut self) -> Result<ConditionExpr, ConditionParseError> {
|
||||
let mut left = self.parse_and_expression()?;
|
||||
|
||||
while self.current.0 == TokenKind::AzureRbac(AzureRbacTokenKind::LogicalOr)
|
||||
|| (self.current.0 == TokenKind::Ident && self.current_text() == "OR")
|
||||
{
|
||||
self.advance()?;
|
||||
let right = self.parse_and_expression()?;
|
||||
left = ConditionExpr::Logical(LogicalExpression {
|
||||
span: EmptySpan,
|
||||
operator: LogicalOperator::Or,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(left)
|
||||
}
|
||||
|
||||
/// Parse AND expression (higher precedence than OR)
|
||||
pub(super) fn parse_and_expression(&mut self) -> Result<ConditionExpr, ConditionParseError> {
|
||||
let mut left = self.parse_unary_expression()?;
|
||||
|
||||
while self.current.0 == TokenKind::AzureRbac(AzureRbacTokenKind::LogicalAnd)
|
||||
|| (self.current.0 == TokenKind::Ident && self.current_text() == "AND")
|
||||
{
|
||||
self.advance()?;
|
||||
let right = self.parse_unary_expression()?;
|
||||
left = ConditionExpr::Logical(LogicalExpression {
|
||||
span: EmptySpan,
|
||||
operator: LogicalOperator::And,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(left)
|
||||
}
|
||||
|
||||
/// Parse unary expression (NOT, Exists)
|
||||
pub(super) fn parse_unary_expression(&mut self) -> Result<ConditionExpr, ConditionParseError> {
|
||||
if self.current.0 == TokenKind::Ident {
|
||||
let text = self.current_text();
|
||||
match text {
|
||||
"NOT" | "!" => {
|
||||
self.advance()?;
|
||||
let operand = self.parse_unary_expression()?;
|
||||
return Ok(ConditionExpr::Unary(UnaryExpression {
|
||||
span: EmptySpan,
|
||||
operator: UnaryOperator::Not,
|
||||
operand: Box::new(operand),
|
||||
}));
|
||||
}
|
||||
"Exists" => {
|
||||
self.advance()?;
|
||||
let operand = self.parse_primary_expression()?;
|
||||
return Ok(ConditionExpr::Unary(UnaryExpression {
|
||||
span: EmptySpan,
|
||||
operator: UnaryOperator::Exists,
|
||||
operand: Box::new(operand),
|
||||
}));
|
||||
}
|
||||
"NotExists" => {
|
||||
self.advance()?;
|
||||
let operand = self.parse_primary_expression()?;
|
||||
return Ok(ConditionExpr::Unary(UnaryExpression {
|
||||
span: EmptySpan,
|
||||
operator: UnaryOperator::NotExists,
|
||||
operand: Box::new(operand),
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.parse_comparison_expression()
|
||||
}
|
||||
|
||||
/// Parse comparison/binary expression
|
||||
pub(super) fn parse_comparison_expression(
|
||||
&mut self,
|
||||
) -> Result<ConditionExpr, ConditionParseError> {
|
||||
let left = self.parse_primary_expression()?;
|
||||
|
||||
// Check for binary operator
|
||||
if self.current.0 == TokenKind::Ident {
|
||||
let op_text = self.current_text().to_string();
|
||||
|
||||
// Check if this is a known operator
|
||||
if let Some(operator) = ConditionOperator::from_name(&op_text) {
|
||||
self.advance()?;
|
||||
let right = self.parse_primary_expression()?;
|
||||
return Ok(ConditionExpr::Binary(BinaryExpression {
|
||||
span: EmptySpan,
|
||||
operator,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(left)
|
||||
}
|
||||
}
|
||||
24
src/languages/azure_rbac/parser/error.rs
Normal file
24
src/languages/azure_rbac/parser/error.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::string::String;
|
||||
|
||||
/// Error types for condition expression parsing
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ConditionParseError {
|
||||
InvalidExpression(String),
|
||||
UnsupportedCondition(String),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ConditionParseError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
ConditionParseError::InvalidExpression(msg) => {
|
||||
write!(f, "Invalid expression: {}", msg)
|
||||
}
|
||||
ConditionParseError::UnsupportedCondition(expr) => {
|
||||
write!(f, "Unsupported condition expression: {}", expr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/languages/azure_rbac/parser/mod.rs
Normal file
9
src/languages/azure_rbac/parser/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
mod condition_parser;
|
||||
mod error;
|
||||
mod primary;
|
||||
|
||||
pub use condition_parser::{parse_condition_expression, ConditionParser};
|
||||
pub use error::ConditionParseError;
|
||||
302
src/languages/azure_rbac/parser/primary.rs
Normal file
302
src/languages/azure_rbac/parser/primary.rs
Normal file
@@ -0,0 +1,302 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::format;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::languages::azure_rbac::ast::{
|
||||
AttributeReference, AttributeSource, BooleanLiteral, ConditionExpr, ConditionOperator,
|
||||
EmptySpan, FunctionCallExpression, IdentifierExpression, ListLiteral, NullLiteral,
|
||||
NumberLiteral, SetLiteral, StringLiteral,
|
||||
};
|
||||
use crate::lexer::{AzureRbacTokenKind, TokenKind};
|
||||
|
||||
use super::condition_parser::ConditionParser;
|
||||
use super::ConditionParseError;
|
||||
|
||||
impl<'source> ConditionParser<'source> {
|
||||
/// Parse primary expression (literals, attribute refs, function calls, parentheses)
|
||||
pub(super) fn parse_primary_expression(
|
||||
&mut self,
|
||||
) -> Result<ConditionExpr, ConditionParseError> {
|
||||
match self.current.0 {
|
||||
// Parenthesized expression
|
||||
TokenKind::Symbol if self.current_text() == "(" => {
|
||||
self.advance()?;
|
||||
let expr = self.parse_or_expression()?;
|
||||
self.expect_symbol(")")?;
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
// Attribute reference (@Source[...])
|
||||
TokenKind::AzureRbac(AzureRbacTokenKind::At) => self.parse_attribute_reference(),
|
||||
|
||||
// String literal
|
||||
TokenKind::String => {
|
||||
let value = self.current_text().to_string();
|
||||
self.advance()?;
|
||||
Ok(ConditionExpr::StringLiteral(StringLiteral {
|
||||
span: EmptySpan,
|
||||
value,
|
||||
}))
|
||||
}
|
||||
|
||||
// Raw string literal
|
||||
TokenKind::RawString => {
|
||||
let value = self.current_text().to_string();
|
||||
self.advance()?;
|
||||
Ok(ConditionExpr::StringLiteral(StringLiteral {
|
||||
span: EmptySpan,
|
||||
value,
|
||||
}))
|
||||
}
|
||||
|
||||
// Number literal
|
||||
TokenKind::Number => {
|
||||
let raw = self.current_text().to_string();
|
||||
self.advance()?;
|
||||
Ok(ConditionExpr::NumberLiteral(NumberLiteral {
|
||||
span: EmptySpan,
|
||||
raw,
|
||||
}))
|
||||
}
|
||||
|
||||
// Set literal {'a', 'b', 'c'}
|
||||
TokenKind::Symbol if self.current_text() == "{" => self.parse_set_literal(),
|
||||
|
||||
// List literal ['a', 'b']
|
||||
TokenKind::Symbol if self.current_text() == "[" => self.parse_list_literal(),
|
||||
|
||||
// Identifier (function call, boolean, identifier)
|
||||
TokenKind::Ident => {
|
||||
let text = self.current_text();
|
||||
match text {
|
||||
"true" => {
|
||||
self.advance()?;
|
||||
Ok(ConditionExpr::BooleanLiteral(BooleanLiteral {
|
||||
span: EmptySpan,
|
||||
value: true,
|
||||
}))
|
||||
}
|
||||
"false" => {
|
||||
self.advance()?;
|
||||
Ok(ConditionExpr::BooleanLiteral(BooleanLiteral {
|
||||
span: EmptySpan,
|
||||
value: false,
|
||||
}))
|
||||
}
|
||||
"null" => {
|
||||
self.advance()?;
|
||||
Ok(ConditionExpr::NullLiteral(NullLiteral { span: EmptySpan }))
|
||||
}
|
||||
_ => {
|
||||
let name = text.to_string();
|
||||
self.advance()?;
|
||||
|
||||
// Check for function call
|
||||
if self.current.0 == TokenKind::Symbol && self.current_text() == "(" {
|
||||
self.parse_function_call(name)
|
||||
} else {
|
||||
if ConditionOperator::from_name(&name).is_some() {
|
||||
return Err(ConditionParseError::UnsupportedCondition(format!(
|
||||
"Operator '{}' missing operands",
|
||||
name
|
||||
)));
|
||||
}
|
||||
|
||||
// Just an identifier
|
||||
Ok(ConditionExpr::Identifier(IdentifierExpression {
|
||||
span: EmptySpan,
|
||||
name,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => Err(ConditionParseError::UnsupportedCondition(format!(
|
||||
"Unexpected token: {:?} '{}'",
|
||||
self.current.0,
|
||||
self.current_text()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse attribute reference @Source[namespace:attribute]
|
||||
pub(super) fn parse_attribute_reference(
|
||||
&mut self,
|
||||
) -> Result<ConditionExpr, ConditionParseError> {
|
||||
self.expect(TokenKind::AzureRbac(AzureRbacTokenKind::At))?;
|
||||
|
||||
// Parse source (Request, Resource, Principal, Environment, Context)
|
||||
if self.current.0 != TokenKind::Ident {
|
||||
return Err(ConditionParseError::UnsupportedCondition(
|
||||
"Expected attribute source after @".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let source_text = self.current_text();
|
||||
let source = match source_text {
|
||||
"Request" => AttributeSource::Request,
|
||||
"Resource" => AttributeSource::Resource,
|
||||
"Principal" => AttributeSource::Principal,
|
||||
"Environment" => AttributeSource::Environment,
|
||||
"Context" => AttributeSource::Context,
|
||||
_ => {
|
||||
return Err(ConditionParseError::UnsupportedCondition(format!(
|
||||
"Unknown attribute source: {}",
|
||||
source_text
|
||||
)))
|
||||
}
|
||||
};
|
||||
self.advance()?;
|
||||
|
||||
// Expect [
|
||||
self.expect_symbol("[")?;
|
||||
|
||||
// Parse namespace:attribute or just attribute
|
||||
let mut namespace = None;
|
||||
|
||||
// Read until ] - this handles namespace:attribute and complex paths
|
||||
let mut parts = Vec::new();
|
||||
loop {
|
||||
match self.current.0 {
|
||||
TokenKind::Symbol if self.current_text() == "]" => break,
|
||||
TokenKind::Symbol if self.current_text() == ":" => {
|
||||
// Colon separator
|
||||
parts.push(":".to_string());
|
||||
self.advance()?;
|
||||
}
|
||||
TokenKind::Ident | TokenKind::String | TokenKind::Symbol => {
|
||||
parts.push(self.current_text().to_string());
|
||||
self.advance()?;
|
||||
}
|
||||
_ => {
|
||||
return Err(ConditionParseError::UnsupportedCondition(format!(
|
||||
"Unexpected token in attribute reference: {:?}",
|
||||
self.current.0
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Join parts and split on last colon
|
||||
let full_path = parts.join("");
|
||||
let attribute = if let Some(colon_pos) = full_path.rfind(':') {
|
||||
namespace = Some(full_path[..colon_pos].to_string());
|
||||
full_path[colon_pos + 1..].to_string()
|
||||
} else {
|
||||
full_path
|
||||
};
|
||||
|
||||
self.expect_symbol("]")?;
|
||||
|
||||
Ok(ConditionExpr::AttributeReference(AttributeReference {
|
||||
span: EmptySpan,
|
||||
source,
|
||||
namespace,
|
||||
attribute,
|
||||
path: Vec::new(), // Path parsing can be added later if needed
|
||||
}))
|
||||
}
|
||||
|
||||
/// Parse function call
|
||||
pub(super) fn parse_function_call(
|
||||
&mut self,
|
||||
function: String,
|
||||
) -> Result<ConditionExpr, ConditionParseError> {
|
||||
self.expect_symbol("(")?;
|
||||
|
||||
let mut arguments = Vec::new();
|
||||
|
||||
// Parse arguments
|
||||
if self.current.0 != TokenKind::Symbol || self.current_text() != ")" {
|
||||
loop {
|
||||
let arg = self.parse_or_expression()?;
|
||||
arguments.push(arg);
|
||||
|
||||
if self.current.0 == TokenKind::Symbol && self.current_text() == "," {
|
||||
self.advance()?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.expect_symbol(")")?;
|
||||
|
||||
Ok(ConditionExpr::FunctionCall(FunctionCallExpression {
|
||||
span: EmptySpan,
|
||||
function,
|
||||
arguments,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Parse set literal {'a', 'b', 'c'}
|
||||
pub(super) fn parse_set_literal(&mut self) -> Result<ConditionExpr, ConditionParseError> {
|
||||
self.expect_symbol("{")?;
|
||||
|
||||
let mut elements = Vec::new();
|
||||
|
||||
if self.current.0 != TokenKind::Symbol || self.current_text() != "}" {
|
||||
loop {
|
||||
let elem = self.parse_or_expression()?;
|
||||
elements.push(elem);
|
||||
|
||||
if self.current.0 == TokenKind::Symbol && self.current_text() == "," {
|
||||
self.advance()?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.expect_symbol("}")?;
|
||||
|
||||
Ok(ConditionExpr::SetLiteral(SetLiteral {
|
||||
span: EmptySpan,
|
||||
elements,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Parse list literal ['a', 'b']
|
||||
pub(super) fn parse_list_literal(&mut self) -> Result<ConditionExpr, ConditionParseError> {
|
||||
self.expect_symbol("[")?;
|
||||
|
||||
let mut elements = Vec::new();
|
||||
|
||||
if self.current.0 != TokenKind::Symbol || self.current_text() != "]" {
|
||||
loop {
|
||||
let elem = self.parse_or_expression()?;
|
||||
elements.push(elem);
|
||||
|
||||
if self.current.0 == TokenKind::Symbol && self.current_text() == "," {
|
||||
self.advance()?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.expect_symbol("]")?;
|
||||
|
||||
Ok(ConditionExpr::ListLiteral(ListLiteral {
|
||||
span: EmptySpan,
|
||||
elements,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Expect a specific symbol
|
||||
pub(super) fn expect_symbol(&mut self, expected: &str) -> Result<(), ConditionParseError> {
|
||||
if self.current.0 != TokenKind::Symbol || self.current_text() != expected {
|
||||
return Err(ConditionParseError::UnsupportedCondition(format!(
|
||||
"Expected '{}', found '{}'",
|
||||
expected,
|
||||
self.current_text()
|
||||
)));
|
||||
}
|
||||
self.advance()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
1091
src/languages/azure_rbac/test_cases.yaml
Normal file
1091
src/languages/azure_rbac/test_cases.yaml
Normal file
File diff suppressed because it is too large
Load Diff
154
src/languages/azure_rbac/tests.rs
Normal file
154
src/languages/azure_rbac/tests.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Tests for Azure RBAC condition expression parsing
|
||||
|
||||
#[cfg(test)]
|
||||
mod condition_tests {
|
||||
use crate::languages::azure_rbac::parser::*;
|
||||
use alloc::string::String;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use serde_json;
|
||||
use serde_yaml;
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct TestCase {
|
||||
name: String,
|
||||
condition: String,
|
||||
expected: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct TestCases {
|
||||
test_cases: Vec<TestCase>,
|
||||
}
|
||||
|
||||
fn load_test_cases() -> TestCases {
|
||||
let yaml_content = include_str!("test_cases.yaml");
|
||||
serde_yaml::from_str(yaml_content).expect("Failed to parse test cases YAML")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_condition_expression_parsing() {
|
||||
let test_cases = load_test_cases();
|
||||
|
||||
for test_case in test_cases.test_cases {
|
||||
// Parse the condition expression
|
||||
let result = parse_condition_expression(&test_case.condition);
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to parse condition '{}' for test '{}': {:?}",
|
||||
test_case.condition,
|
||||
test_case.name,
|
||||
result.err()
|
||||
);
|
||||
|
||||
let parsed_condition = result.unwrap();
|
||||
|
||||
// Verify we have a parsed expression
|
||||
assert!(
|
||||
parsed_condition.expression.is_some(),
|
||||
"No parsed expression for test '{}', condition: '{}'",
|
||||
test_case.name,
|
||||
test_case.condition
|
||||
);
|
||||
|
||||
let actual_expr = parsed_condition.expression.unwrap();
|
||||
|
||||
// Deserialize the expected JSON into our AST type
|
||||
let expected_expr: crate::languages::azure_rbac::ast::ConditionExpr =
|
||||
serde_json::from_value(test_case.expected.clone()).unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Failed to deserialize expected JSON for test '{}': {}",
|
||||
test_case.name,
|
||||
serde_json::to_string_pretty(&test_case.expected).unwrap()
|
||||
)
|
||||
});
|
||||
|
||||
// Compare the actual parsed expression with the expected expression
|
||||
assert_eq!(
|
||||
actual_expr, expected_expr,
|
||||
"\nTest '{}' failed!\nCondition: '{}'\nExpected: {:#?}\nActual: {:#?}",
|
||||
test_case.name, test_case.condition, expected_expr, actual_expr
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test for edge cases and error conditions
|
||||
#[test]
|
||||
fn test_parsing_errors() {
|
||||
// The parser funnels every binary/logical keyword through the same branches,
|
||||
// so this list of malformed expressions covers all operators without duplication.
|
||||
let invalid_expressions = vec![
|
||||
("", "Empty expression"),
|
||||
("@", "Incomplete attribute reference"),
|
||||
("@Request", "Missing attribute brackets"),
|
||||
("@Request[", "Unclosed attribute brackets"),
|
||||
("@InvalidSource[attr]", "Invalid attribute source"),
|
||||
("@Request[attr] InvalidOperator 'value'", "Invalid operator"),
|
||||
("@Request[attr] StringEquals", "Missing right operand"),
|
||||
("StringEquals 'value'", "Missing left operand"),
|
||||
(
|
||||
"@Request[attr] 'value'",
|
||||
"Missing operator between operands",
|
||||
),
|
||||
("StringEquals 'value'", "Missing left operand"),
|
||||
("StringEquals", "Binary operator without any operands"),
|
||||
(
|
||||
"@Request[attr] StringEquals 'value' AND",
|
||||
"Missing right operand after AND",
|
||||
),
|
||||
(
|
||||
"AND @Request[attr] StringEquals 'value'",
|
||||
"Leading AND without left operand",
|
||||
),
|
||||
(
|
||||
"@Request[attr] StringEquals 'value' OR",
|
||||
"Missing right operand after OR",
|
||||
),
|
||||
(
|
||||
"OR @Request[attr] StringEquals 'value'",
|
||||
"Leading OR without left operand",
|
||||
),
|
||||
("NOT", "Standalone NOT without operand"),
|
||||
("Exists", "Exists without operand"),
|
||||
("NotExists", "NotExists without operand"),
|
||||
(
|
||||
"(@Request[attr] StringEquals 'value'",
|
||||
"Unmatched parenthesis",
|
||||
),
|
||||
("@Request[attr] StringEquals 'unclosed", "Unclosed string"),
|
||||
(
|
||||
"@Request[attr] StringEquals 'value')",
|
||||
"Unexpected closing parenthesis",
|
||||
),
|
||||
];
|
||||
|
||||
for (expression, description) in invalid_expressions {
|
||||
let result = parse_condition_expression(expression);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected error for {} but got success: {:?}",
|
||||
description,
|
||||
result
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_expression_preservation() {
|
||||
let test_cases = load_test_cases();
|
||||
|
||||
for test_case in test_cases.test_cases.iter().take(5) {
|
||||
// Test first 5 cases
|
||||
let result = parse_condition_expression(&test_case.condition).unwrap();
|
||||
assert_eq!(
|
||||
result.raw_expression, test_case.condition,
|
||||
"Raw expression not preserved for test '{}'",
|
||||
test_case.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/languages/mod.rs
Normal file
7
src/languages/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Language-specific modules for specialized parsing and evaluation
|
||||
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
pub mod azure_rbac;
|
||||
140
src/lexer.rs
140
src/lexer.rs
@@ -274,6 +274,14 @@ impl Debug for Span {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum AzureRbacTokenKind {
|
||||
At, // @ symbol for attribute sources (@Request, @Resource, etc.)
|
||||
LogicalAnd, // && operator
|
||||
LogicalOr, // || operator
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum TokenKind {
|
||||
Symbol,
|
||||
@@ -282,6 +290,9 @@ pub enum TokenKind {
|
||||
Number,
|
||||
Ident,
|
||||
Eof,
|
||||
// Azure RBAC-specific tokens
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
AzureRbac(AzureRbacTokenKind),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -297,6 +308,10 @@ pub struct Lexer<'source> {
|
||||
allow_slash_star_escape: bool,
|
||||
comment_starts_with_double_slash: bool,
|
||||
double_colon_token: bool,
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
enable_rbac_tokens: bool,
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
allow_single_quoted_strings: bool,
|
||||
}
|
||||
|
||||
impl<'source> Lexer<'source> {
|
||||
@@ -310,6 +325,10 @@ impl<'source> Lexer<'source> {
|
||||
allow_slash_star_escape: false,
|
||||
comment_starts_with_double_slash: false,
|
||||
double_colon_token: false,
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
enable_rbac_tokens: false,
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
allow_single_quoted_strings: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,6 +348,16 @@ impl<'source> Lexer<'source> {
|
||||
self.double_colon_token = b;
|
||||
}
|
||||
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
pub fn set_enable_rbac_tokens(&mut self, b: bool) {
|
||||
self.enable_rbac_tokens = b;
|
||||
}
|
||||
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
pub fn set_allow_single_quoted_strings(&mut self, b: bool) {
|
||||
self.allow_single_quoted_strings = b;
|
||||
}
|
||||
|
||||
fn peek(&mut self) -> (usize, char) {
|
||||
match self.iter.peek() {
|
||||
Some((index, chr)) => (*index, *chr),
|
||||
@@ -572,6 +601,60 @@ impl<'source> Lexer<'source> {
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
fn read_single_quoted_string(&mut self) -> Result<Token> {
|
||||
let (line, col) = (self.line, self.col);
|
||||
self.iter.next();
|
||||
self.col += 1;
|
||||
let (start, _) = self.peek();
|
||||
loop {
|
||||
let (offset, ch) = self.peek();
|
||||
let col = self.col + (offset - start) as u32;
|
||||
match ch {
|
||||
'\'' | '\x00' => {
|
||||
break;
|
||||
}
|
||||
'\\' => {
|
||||
self.iter.next();
|
||||
let (_, ch) = self.peek();
|
||||
self.iter.next();
|
||||
match ch {
|
||||
// Basic escape sequences for single-quoted strings
|
||||
'\'' | '\\' | 'n' | 'r' | 't' => (),
|
||||
_ => return Err(self.source.error(line, col, "invalid escape sequence")),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// check for valid chars
|
||||
let col = self.col + (offset - start) as u32;
|
||||
if !('\u{0020}'..='\u{10FFFF}').contains(&ch) {
|
||||
return Err(self.source.error(line, col, "invalid character in string"));
|
||||
}
|
||||
self.iter.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.peek().1 != '\'' {
|
||||
return Err(self.source.error(line, col, "unmatched '"));
|
||||
}
|
||||
|
||||
self.iter.next();
|
||||
let end = self.peek().0;
|
||||
self.col += (end - start) as u32;
|
||||
|
||||
Ok(Token(
|
||||
TokenKind::String,
|
||||
Span {
|
||||
source: self.source.clone(),
|
||||
line,
|
||||
col: col + 1,
|
||||
start: start as u32,
|
||||
end: end as u32 - 1,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn skip_past_newline(&mut self) -> Result<()> {
|
||||
self.iter.next();
|
||||
@@ -638,8 +721,6 @@ impl<'source> Lexer<'source> {
|
||||
'{' | '}' | '[' | ']' | '(' | ')' |
|
||||
// arith operator
|
||||
'+' | '-' | '*' | '/' | '%' |
|
||||
// bin operator
|
||||
'&' | '|' |
|
||||
// separators
|
||||
',' | ';' | '.' => {
|
||||
self.col += 1;
|
||||
@@ -652,6 +733,46 @@ impl<'source> Lexer<'source> {
|
||||
end: start as u32 + 1,
|
||||
}))
|
||||
}
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
// RBAC logical AND operator (&&)
|
||||
'&' if self.enable_rbac_tokens && self.peekahead(1).1 == '&' => {
|
||||
self.col += 2;
|
||||
self.iter.next();
|
||||
self.iter.next();
|
||||
Ok(Token(TokenKind::AzureRbac(AzureRbacTokenKind::LogicalAnd), Span {
|
||||
source: self.source.clone(),
|
||||
line: self.line,
|
||||
col,
|
||||
start: start as u32,
|
||||
end: start as u32 + 2,
|
||||
}))
|
||||
}
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
// RBAC logical OR operator (||)
|
||||
'|' if self.enable_rbac_tokens && self.peekahead(1).1 == '|' => {
|
||||
self.col += 2;
|
||||
self.iter.next();
|
||||
self.iter.next();
|
||||
Ok(Token(TokenKind::AzureRbac(AzureRbacTokenKind::LogicalOr), Span {
|
||||
source: self.source.clone(),
|
||||
line: self.line,
|
||||
col,
|
||||
start: start as u32,
|
||||
end: start as u32 + 2,
|
||||
}))
|
||||
}
|
||||
// Generic bin operators (when RBAC tokens not enabled or single & |)
|
||||
'&' | '|' => {
|
||||
self.col += 1;
|
||||
self.iter.next();
|
||||
Ok(Token(TokenKind::Symbol, Span {
|
||||
source: self.source.clone(),
|
||||
line: self.line,
|
||||
col,
|
||||
start: start as u32,
|
||||
end: start as u32 + 1,
|
||||
}))
|
||||
}
|
||||
':' => {
|
||||
self.col += 1;
|
||||
self.iter.next();
|
||||
@@ -697,7 +818,22 @@ impl<'source> Lexer<'source> {
|
||||
end: self.peek().0 as u32,
|
||||
}))
|
||||
}
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
// RBAC @ token for attribute references
|
||||
'@' if self.enable_rbac_tokens => {
|
||||
self.col += 1;
|
||||
self.iter.next();
|
||||
Ok(Token(TokenKind::AzureRbac(AzureRbacTokenKind::At), Span {
|
||||
source: self.source.clone(),
|
||||
line: self.line,
|
||||
col,
|
||||
start: start as u32,
|
||||
end: start as u32 + 1,
|
||||
}))
|
||||
}
|
||||
'"' => self.read_string(),
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
'\'' if self.allow_single_quoted_strings => self.read_single_quoted_string(),
|
||||
'`' => self.read_raw_string(),
|
||||
'\x00' => Ok(Token(TokenKind::Eof, Span {
|
||||
source: self.source.clone(),
|
||||
|
||||
@@ -30,6 +30,12 @@ mod compiler;
|
||||
mod engine;
|
||||
mod indexchecker;
|
||||
mod interpreter;
|
||||
|
||||
pub mod languages {
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
pub mod azure_rbac;
|
||||
}
|
||||
|
||||
mod lexer;
|
||||
mod lookup;
|
||||
mod number;
|
||||
|
||||
@@ -82,10 +82,8 @@ impl<'source> Parser<'source> {
|
||||
|
||||
pub fn token_text(&self) -> &str {
|
||||
match self.tok.0 {
|
||||
TokenKind::Symbol | TokenKind::Number | TokenKind::Ident | TokenKind::Eof => {
|
||||
self.tok.1.text()
|
||||
}
|
||||
TokenKind::String | TokenKind::RawString => "",
|
||||
_ => self.tok.1.text(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user