use crate::model::{ConditionOperator, Effect, Policy, QString};
use crate::offline::policy::evaluate_policy;
use std::fmt::{Display, Error, Formatter};
use tracing::instrument;
#[derive(Clone, Debug, PartialEq)]
pub enum EvaluationError {
UnknownOperator(String),
UnknownVariableName(String),
InvalidVariableName(String),
ExpectingVariableType(String),
MissingVariableValue(String),
InvalidValueCardinality,
Errors(Vec<EvaluationError>),
}
#[derive(Clone, Debug, PartialEq)]
pub enum Source {
Default,
Principal,
NotPrincipal,
Action,
NotAction,
Resource,
NotResource,
Condition(ConditionOperator, QString),
}
#[derive(Debug, PartialEq)]
pub enum EvaluationResult {
Allow,
Deny(Source, String),
}
type PartialEvaluationResult = Option<EvaluationResult>;
pub fn evaluate(request: &Request, policy: &Policy) -> Result<EvaluationResult, EvaluationError> {
evaluate_all(request, &[policy])
}
#[instrument]
pub fn evaluate_all(
request: &Request,
policies: &[&Policy],
) -> Result<EvaluationResult, EvaluationError> {
let results: Result<Vec<PartialEvaluationResult>, EvaluationError> = policies
.iter()
.enumerate()
.map(|(idx, policy)| evaluate_policy(request, policy, idx as i32))
.collect();
match results {
Ok(mut results) => Ok(reduce_results(&mut results)),
Err(err) => Err(err),
}
}
impl Display for EvaluationResult {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
match self {
Self::Allow => write!(f, "Request allowed"),
Self::Deny(source, message) => match source {
Source::Condition(op, key) => write!(
f,
"Request denied, statement condition operator {:?} for key {:?}, message: {}",
op, key, message
),
_ => write!(
f,
"Request denied, statement source {:?}, message: {}",
source, message
),
},
}
}
}
impl Into<Effect> for EvaluationResult {
fn into(self) -> Effect {
match self {
Self::Allow => Effect::Allow,
Self::Deny(_, _) => Effect::Deny,
}
}
}
fn reduce_results(results: &mut Vec<PartialEvaluationResult>) -> EvaluationResult {
match reduce_optional_results(results) {
None => EvaluationResult::Deny(Source::Default, "no explicit effect set".to_string()),
Some(result) => result,
}
}
pub(crate) fn reduce_optional_results(
results: &mut Vec<PartialEvaluationResult>,
) -> PartialEvaluationResult {
let effect_or_none: PartialEvaluationResult =
results.drain(0..).fold(None, |acc, result| match result {
Some(EvaluationResult::Allow) => {
if let Some(EvaluationResult::Deny(_, _)) = acc {
acc
} else {
Some(EvaluationResult::Allow)
}
}
Some(EvaluationResult::Deny(s, m)) => Some(EvaluationResult::Deny(s, m)),
_ => acc,
});
effect_or_none
}
mod policy;
mod statement;
mod operators;
mod request;
pub use request::{Environment, Principal, Request};
mod variables;
#[cfg(test)]
mod tests {
use crate::constants;
use crate::io;
use crate::model::{ConditionValue, QString};
use crate::offline::{
evaluate, request::Environment, EvaluationResult, Principal, Request, Source,
};
use std::str::FromStr;
fn make_request(
test_case: &str,
principal: Option<Principal>,
action: &str,
resource: &str,
) -> Request {
let environment: Environment = [
(
QString::from_str(constants::AWS_EPOCH_TIME).unwrap(),
ConditionValue::Integer(1000),
),
(
QString::from_str(constants::AWS_REQUESTED_REGION).unwrap(),
ConditionValue::String("us-east-1".to_string()),
),
(
QString::from_str(constants::AWS_SECURE_TRANSPORT).unwrap(),
ConditionValue::Bool(true),
),
]
.iter()
.cloned()
.collect();
Request {
request_id: Some(String::from(test_case)),
principal,
action: QString::from_str(action).unwrap(),
resource: String::from(resource),
environment,
}
}
#[test]
fn test_serialize_resource() {
let request = make_request(
"test_deny_resource_string_match",
None,
"dynamodb:read",
"arn:aws:dynamodb:us-east-2:123456789012:table/NotBooks",
);
println!("{}", serde_json::to_string_pretty(&request).unwrap());
}
#[test]
fn test_deny_resource_string_match() {
let policy = r#"{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": "arn:aws:dynamodb:us-east-2:123456789012:table/Books"
}
}"#;
let policy = io::read_from_string(policy).expect("error parsing policy");
let request = make_request(
"test_deny_resource_string_match",
None,
"dynamodb:read",
"arn:aws:dynamodb:us-east-2:123456789012:table/NotBooks",
);
let result = evaluate(&request, &policy);
assert_eq!(
result,
Ok(EvaluationResult::Deny(
Source::Resource,
String::from("string_match")
))
);
}
#[test]
fn test_deny_action_qstring_match() {
let policy = r#"{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": "arn:aws:dynamodb:us-east-2:123456789012:table/Books"
}
}"#;
let policy = io::read_from_string(policy).expect("error parsing policy");
let request = make_request(
"test_deny_action_qstring_match",
None,
"s3:read",
"arn:aws:dynamodb:us-east-2:123456789012:table/Books",
);
let result = evaluate(&request, &policy);
assert_eq!(
result,
Ok(EvaluationResult::Deny(
Source::Action,
String::from("string_match")
))
);
}
#[test]
fn test_simple_allow() {
let policy = r#"{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": "arn:aws:dynamodb:us-east-2:123456789012:table/Books"
}
}"#;
let policy = io::read_from_string(policy).expect("error parsing policy");
let request = make_request(
"test_simple_allow",
None,
"dynamodb:read",
"arn:aws:dynamodb:us-east-2:123456789012:table/Books",
);
let result = evaluate(&request, &policy);
assert_eq!(result, Ok(EvaluationResult::Allow));
}
}