Skip to content

Commit

Permalink
Policy engine proptests (Azure#3985)
Browse files Browse the repository at this point in the history
In this PR I've added one proptest scenario for Policy engine.

To make that work I had to introduce a method `PolicyBuilder::from_definition(definition)`, that can accept struct, instead of json string.

Proptests guarder by the "proptest" feature.
  • Loading branch information
vadim-kovalyov authored Nov 19, 2020
1 parent 6107589 commit d3defcc
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 64 deletions.
2 changes: 2 additions & 0 deletions mqtt/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions mqtt/policy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ version = "0.1.0"

[dependencies]
lazy_static = "1"
proptest = { version = "0.9", optional = true }
regex = "1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"

[dev-dependencies]
matches = "0.1"
itertools = "0.9"
109 changes: 69 additions & 40 deletions mqtt/policy/src/core/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ pub struct PolicyBuilder<V, M, S> {
validator: V,
matcher: M,
substituter: S,
json: String,
source: Source,
default_decision: Decision,
}

impl PolicyBuilder<DefaultValidator, DefaultResourceMatcher, DefaultSubstituter> {
/// Constructs a `PolicyBuilder` from provided json policy definition and
/// Constructs a `PolicyBuilder` from provided json policy definition, with
/// default configuration.
///
/// Call to this method does not parse or validate the json, all heavy work
Expand All @@ -33,7 +33,24 @@ impl PolicyBuilder<DefaultValidator, DefaultResourceMatcher, DefaultSubstituter>
json: impl Into<String>,
) -> PolicyBuilder<DefaultValidator, DefaultResourceMatcher, DefaultSubstituter> {
PolicyBuilder {
json: json.into(),
source: Source::Json(json.into()),
validator: DefaultValidator,
matcher: DefaultResourceMatcher,
substituter: DefaultSubstituter,
default_decision: Decision::Denied,
}
}

/// Constructs a `PolicyBuilder` from provided policy definition struct, with
/// default configuration.
///
/// Call to this method does not validate the definition, all heavy work
/// is done in `build` method.
pub fn from_definition(
definition: PolicyDefinition,
) -> PolicyBuilder<DefaultValidator, DefaultResourceMatcher, DefaultSubstituter> {
PolicyBuilder {
source: Source::Definition(definition),
validator: DefaultValidator,
matcher: DefaultResourceMatcher,
substituter: DefaultSubstituter,
Expand All @@ -52,7 +69,7 @@ where
/// Specifies the `PolicyValidator` to validate the policy definition.
pub fn with_validator<V1>(self, validator: V1) -> PolicyBuilder<V1, M, S> {
PolicyBuilder {
json: self.json,
source: self.source,
validator,
matcher: self.matcher,
substituter: self.substituter,
Expand All @@ -63,7 +80,7 @@ where
/// Specifies the `ResourceMatcher` to use with `Policy`.
pub fn with_matcher<M1>(self, matcher: M1) -> PolicyBuilder<V, M1, S> {
PolicyBuilder {
json: self.json,
source: self.source,
validator: self.validator,
matcher,
substituter: self.substituter,
Expand All @@ -74,7 +91,7 @@ where
/// Specifies the `Substituter` to use with `Policy`.
pub fn with_substituter<S1>(self, substituter: S1) -> PolicyBuilder<V, M, S1> {
PolicyBuilder {
json: self.json,
source: self.source,
validator: self.validator,
matcher: self.matcher,
substituter,
Expand All @@ -96,13 +113,26 @@ where
///
/// Any validation errors are collected and returned as `Error::ValidationSummary`.
pub fn build(self) -> Result<Policy<M, S>> {
let mut definition: PolicyDefinition = PolicyDefinition::from_json(&self.json)?;
let PolicyBuilder {
validator,
matcher,
substituter,
source,
default_decision,
} = self;

let mut definition: PolicyDefinition = match source {
Source::Json(json) => PolicyDefinition::from_json(&json)?,
Source::Definition(definition) => definition,
};

for (order, mut statement) in definition.statements.iter_mut().enumerate() {
statement.order = order;
}

self.validate(&definition)?;
validator
.validate(&definition)
.map_err(|e| Error::Validation(e.into()))?;

let mut static_rules = Identities::new();
let mut variable_rules = Identities::new();
Expand All @@ -112,19 +142,13 @@ where
}

Ok(Policy {
default_decision: self.default_decision,
resource_matcher: self.matcher,
substituter: self.substituter,
default_decision,
resource_matcher: matcher,
substituter,
static_rules: static_rules.0,
variable_rules: variable_rules.0,
})
}

fn validate(&self, definition: &PolicyDefinition) -> Result<()> {
self.validator
.validate(definition)
.map_err(|e| Error::Validation(e.into()))
}
}

fn process_statement(
Expand Down Expand Up @@ -215,11 +239,16 @@ fn is_variable_rule(value: &str) -> bool {
VAR_PATTERN.is_match(value)
}

enum Source {
Json(String),
Definition(PolicyDefinition),
}

/// Represents a deserialized policy definition.
#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PolicyDefinition {
statements: Vec<Statement>,
pub(super) statements: Vec<Statement>,
}

impl PolicyDefinition {
Expand All @@ -236,18 +265,18 @@ impl PolicyDefinition {
}

/// Represents a statement in a policy definition.
#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Statement {
#[serde(default)]
order: usize,
pub(super) order: usize,
#[serde(default)]
description: String,
effect: Effect,
identities: Vec<String>,
operations: Vec<String>,
pub(super) description: String,
pub(super) effect: Effect,
pub(super) identities: Vec<String>,
pub(super) operations: Vec<String>,
#[serde(default)]
resources: Vec<String>,
pub(super) resources: Vec<String>,
}

impl Statement {
Expand Down Expand Up @@ -277,7 +306,7 @@ impl Statement {
}

/// Represents an effect on a statement.
#[derive(Deserialize, Copy, Clone)]
#[derive(Debug, Deserialize, Copy, Clone)]
#[serde(rename_all = "camelCase")]
pub enum Effect {
Allow,
Expand All @@ -291,7 +320,7 @@ mod tests {
use matches::assert_matches;

use crate::{
core::{tests::build_policy, Effect as CoreEffect, EffectOrd},
core::{tests::build_policy, EffectImpl, EffectOrd},
validator::ValidatorError,
};

Expand Down Expand Up @@ -544,7 +573,7 @@ mod tests {
assert_eq!(
EffectOrd {
order: 0,
effect: CoreEffect::Allow
effect: EffectImpl::Allow
},
policy.static_rules["actor_a"].0["write"].0["events/telemetry"]
);
Expand All @@ -553,7 +582,7 @@ mod tests {
assert_eq!(
EffectOrd {
order: 2,
effect: CoreEffect::Allow
effect: EffectImpl::Allow
},
policy.variable_rules["actor_a"].0["read"].0["{{variable}}/#"]
);
Expand Down Expand Up @@ -591,28 +620,28 @@ mod tests {
assert_eq!(
policy.static_rules["actor_a"].0["write"].0["events/telemetry"],
EffectOrd {
effect: CoreEffect::Allow,
effect: EffectImpl::Allow,
order: 0
}
);
assert_eq!(
policy.static_rules["actor_a"].0["read"].0["events/telemetry"],
EffectOrd {
effect: CoreEffect::Allow,
effect: EffectImpl::Allow,
order: 0
}
);
assert_eq!(
policy.static_rules["actor_b"].0["write"].0["events/telemetry"],
EffectOrd {
effect: CoreEffect::Allow,
effect: EffectImpl::Allow,
order: 0
}
);
assert_eq!(
policy.static_rules["actor_b"].0["read"].0["events/telemetry"],
EffectOrd {
effect: CoreEffect::Allow,
effect: EffectImpl::Allow,
order: 0
}
);
Expand All @@ -622,42 +651,42 @@ mod tests {
assert_eq!(
policy.variable_rules["actor_a"].0["write"].0["devices/{{variable}}/#"],
EffectOrd {
effect: CoreEffect::Allow,
effect: EffectImpl::Allow,
order: 0
}
);
assert_eq!(
policy.variable_rules["actor_a"].0["read"].0["devices/{{variable}}/#"],
EffectOrd {
effect: CoreEffect::Allow,
effect: EffectImpl::Allow,
order: 0
}
);
assert_eq!(
policy.variable_rules["actor_b"].0["write"].0["devices/{{variable}}/#"],
EffectOrd {
effect: CoreEffect::Allow,
effect: EffectImpl::Allow,
order: 0
}
);
assert_eq!(
policy.variable_rules["actor_b"].0["read"].0["devices/{{variable}}/#"],
EffectOrd {
effect: CoreEffect::Allow,
effect: EffectImpl::Allow,
order: 0
}
);
assert_eq!(
policy.variable_rules["{{var_actor}}"].0["write"].0["devices/{{variable}}/#"],
EffectOrd {
effect: CoreEffect::Allow,
effect: EffectImpl::Allow,
order: 0
}
);
assert_eq!(
policy.variable_rules["{{var_actor}}"].0["read"].0["devices/{{variable}}/#"],
EffectOrd {
effect: CoreEffect::Allow,
effect: EffectImpl::Allow,
order: 0
}
);
Expand Down
Loading

0 comments on commit d3defcc

Please sign in to comment.