use bon::Builder;
use serde::Serialize;
use serde_json;
use std::collections::HashMap;
#[derive(Debug, Default, Clone, Serialize, PartialEq, Eq)]
pub enum PolicyVersion {
#[serde(rename = "1")]
#[default]
V1,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub enum Effect {
Allow,
Deny,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}
impl<T> From<T> for OneOrMany<T> {
fn from(value: T) -> Self {
OneOrMany::One(value)
}
}
impl<T> From<Vec<T>> for OneOrMany<T> {
fn from(values: Vec<T>) -> Self {
OneOrMany::Many(values)
}
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)]
#[serde(transparent)]
pub struct ConditionValue(pub String);
impl From<String> for ConditionValue {
fn from(s: String) -> Self {
ConditionValue(s)
}
}
impl From<&str> for ConditionValue {
fn from(s: &str) -> Self {
ConditionValue(s.to_owned())
}
}
impl From<bool> for ConditionValue {
fn from(b: bool) -> Self {
ConditionValue(if b { "true".into() } else { "false".into() })
}
}
impl From<i64> for ConditionValue {
fn from(n: i64) -> Self {
ConditionValue(n.to_string())
}
}
impl From<u64> for ConditionValue {
fn from(n: u64) -> Self {
ConditionValue(n.to_string())
}
}
pub type ConditionOperator = String; pub type ConditionKey = String; pub type ConditionMap =
HashMap<ConditionOperator, HashMap<ConditionKey, OneOrMany<ConditionValue>>>;
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(transparent)]
pub struct ConditionBlock(pub ConditionMap);
impl ConditionBlock {
pub fn new() -> Self {
ConditionBlock(HashMap::new())
}
pub fn insert(
&mut self,
op: impl Into<String>,
key: impl Into<String>,
values: impl Into<OneOrMany<ConditionValue>>,
) {
let op = op.into();
let key = key.into();
let entry = self.0.entry(op).or_default();
entry.insert(key, values.into());
}
}
impl Default for ConditionBlock {
fn default() -> Self {
Self::new()
}
}
pub mod condition_ops {
pub const STRING_EQUALS: &str = "StringEquals";
pub const STRING_NOT_EQUALS: &str = "StringNotEquals";
pub const STRING_EQUALS_IGNORE_CASE: &str = "StringEqualsIgnoreCase";
pub const STRING_NOT_EQUALS_IGNORE_CASE: &str = "StringNotEqualsIgnoreCase";
pub const STRING_LIKE: &str = "StringLike";
pub const STRING_NOT_LIKE: &str = "StringNotLike";
pub const NUMERIC_EQUALS: &str = "NumericEquals";
pub const NUMERIC_NOT_EQUALS: &str = "NumericNotEquals";
pub const NUMERIC_LESS_THAN: &str = "NumericLessThan";
pub const NUMERIC_LESS_THAN_EQUALS: &str = "NumericLessThanEquals";
pub const NUMERIC_GREATER_THAN: &str = "NumericGreaterThan";
pub const NUMERIC_GREATER_THAN_EQUALS: &str = "NumericGreaterThanEquals";
pub const DATE_EQUALS: &str = "DateEquals";
pub const DATE_NOT_EQUALS: &str = "DateNotEquals";
pub const DATE_LESS_THAN: &str = "DateLessThan";
pub const DATE_LESS_THAN_EQUALS: &str = "DateLessThanEquals";
pub const DATE_GREATER_THAN: &str = "DateGreaterThan";
pub const DATE_GREATER_THAN_EQUALS: &str = "DateGreaterThanEquals";
pub const BOOL: &str = "Bool";
pub const IP_ADDRESS: &str = "IpAddress";
pub const NOT_IP_ADDRESS: &str = "NotIpAddress";
pub const IP_ADDRESS_INCLUDE_BORDER: &str = "IpAddressIncludeBorder";
pub const NOT_IP_ADDRESS_INCLUDE_BORDER: &str = "NotIpAddressIncludeBorder";
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
pub struct Statement {
pub effect: Effect,
pub action: Option<OneOrMany<String>>,
pub not_action: Option<OneOrMany<String>>,
pub resource: OneOrMany<String>,
pub condition: Option<ConditionBlock>,
}
#[derive(Builder, Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
pub struct Policy {
#[builder(field)]
pub statement: Vec<Statement>,
#[builder(default)]
pub version: PolicyVersion,
}
#[derive(thiserror::Error, Debug)]
pub enum PolicyValidationError {
#[error("statement must contain either Action or NotAction")]
MissingActionAndNotAction,
#[error("statement cannot contain both Action and NotAction")]
BothActionAndNotActionPresent,
}
impl Statement {
fn validate(&self) -> Result<(), PolicyValidationError> {
match (&self.action, &self.not_action) {
(None, None) => Err(PolicyValidationError::MissingActionAndNotAction),
(Some(_), Some(_)) => Err(PolicyValidationError::BothActionAndNotActionPresent),
_ => Ok(()),
}
}
}
impl<S: policy_builder::State> PolicyBuilder<S> {
pub fn statement(mut self, stmt: Statement) -> Result<Self, PolicyValidationError> {
stmt.validate()?;
self.statement.push(stmt);
Ok(self)
}
pub fn statements(
mut self,
stmts: impl IntoIterator<Item = Statement>,
) -> Result<Self, PolicyValidationError> {
for stmt in stmts.into_iter() {
stmt.validate()?;
self.statement.push(stmt);
}
Ok(self)
}
}
impl Policy {
pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn to_json_string_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
}
#[test]
fn build_policy_test() {
let mut cond = ConditionBlock::new();
cond.insert(
condition_ops::STRING_EQUALS,
"acs:ResourceTag/team",
ConditionValue::from("dev"),
);
let stmt = Statement {
effect: Effect::Allow,
action: Some(OneOrMany::One("ecs:*".to_string())),
not_action: None,
resource: OneOrMany::One("*".to_string()),
condition: Some(cond),
};
let policy = Policy::builder().statement(stmt).unwrap().build();
println!("policy json:\n{}", policy.to_json_string_pretty().unwrap());
let mut cond = ConditionBlock::new();
cond.insert(
condition_ops::NUMERIC_LESS_THAN_EQUALS,
"kms:RecoveryWindowInDays",
ConditionValue::from(10_i64),
);
let stmt = Statement {
effect: Effect::Deny,
action: Some(OneOrMany::One("kms:DeleteSecret".to_string())),
not_action: None,
resource: OneOrMany::One("*".to_string()),
condition: Some(cond),
};
let policy = Policy::builder().statement(stmt).unwrap().build();
println!("policy json:\n{}", policy.to_json_string_pretty().unwrap());
let mut cond = ConditionBlock::new();
cond.insert(
condition_ops::DATE_LESS_THAN,
"acs:CurrentTime",
ConditionValue::from("2019-08-12T17:00:00+08:00"),
);
let stmt = Statement {
effect: Effect::Deny,
action: Some(OneOrMany::One("oss:DeleteObject".to_string())),
not_action: None,
resource: OneOrMany::One("acs:oss:*:*:mybucket/myobject".to_string()),
condition: Some(cond),
};
let s = Policy::builder().statement(stmt).unwrap().build();
println!("policy json:\n{}", s.to_json_string_pretty().unwrap());
}