From 2126c36a73efd0e1c5110a80e07a12dde6abbd4c Mon Sep 17 00:00:00 2001 From: bestgopher <84328409@qq.com> Date: Mon, 23 Dec 2024 21:46:25 +0800 Subject: [PATCH] test(iam): add policy_is_allowed --- ecstore/src/heal/background_heal_ops.rs | 2 +- iam/src/auth/credentials.rs | 1 - iam/src/policy.rs | 6 +- iam/src/policy/action.rs | 21 +- iam/src/policy/function.rs | 286 +++++++++-- iam/src/policy/function/addr.rs | 31 +- iam/src/policy/function/bool_null.rs | 57 ++- iam/src/policy/function/condition.rs | 106 +++- iam/src/policy/function/date.rs | 35 +- iam/src/policy/function/func.rs | 57 +-- iam/src/policy/function/key.rs | 14 +- iam/src/policy/function/key_name.rs | 58 ++- iam/src/policy/function/number.rs | 45 +- iam/src/policy/function/string.rs | 243 +++++++++- iam/src/policy/policy.rs | 34 +- iam/src/policy/resource.rs | 34 +- iam/src/policy/statement.rs | 10 +- iam/src/policy/utils/wildcard.rs | 10 +- iam/src/store/object.rs | 1 - iam/tests/policy_is_allowed.rs | 615 ++++++++++++++++++++++++ 20 files changed, 1454 insertions(+), 212 deletions(-) create mode 100644 iam/tests/policy_is_allowed.rs diff --git a/ecstore/src/heal/background_heal_ops.rs b/ecstore/src/heal/background_heal_ops.rs index 7cd54ee34..91156bee8 100644 --- a/ecstore/src/heal/background_heal_ops.rs +++ b/ecstore/src/heal/background_heal_ops.rs @@ -108,7 +108,7 @@ async fn monitor_local_disks_and_heal() { info!("heal local disks: {:?}", heal_disks); let store = new_object_layer_fn().expect("errServerNotInitialized"); - if let (result, Some(err)) = store.heal_format(false).await.expect("heal format failed") { + if let (_result, Some(err)) = store.heal_format(false).await.expect("heal format failed") { error!("heal local disk format error: {}", err); if let Some(DiskError::NoHealRequired) = err.downcast_ref::() { } else { diff --git a/iam/src/auth/credentials.rs b/iam/src/auth/credentials.rs index 822f237b1..e5d47140a 100644 --- a/iam/src/auth/credentials.rs +++ b/iam/src/auth/credentials.rs @@ -1,7 +1,6 @@ use crate::policy::{Policy, Validator}; use crate::service_type::ServiceType; use crate::{utils, Error}; -use jsonwebtoken::{encode, Algorithm, Header}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::cell::LazyCell; diff --git a/iam/src/policy.rs b/iam/src/policy.rs index b60b017bf..0470ae734 100644 --- a/iam/src/policy.rs +++ b/iam/src/policy.rs @@ -4,8 +4,8 @@ mod effect; mod function; mod id; mod policy; -mod resource; -mod statement; +pub mod resource; +pub mod statement; pub(crate) mod utils; use action::Action; @@ -17,7 +17,7 @@ pub use id::ID; pub use policy::{default::DEFAULT_POLICIES, Policy}; pub use resource::ResourceSet; use serde::{Deserialize, Serialize}; -use serde_json::{to_string, Value}; +use serde_json::Value; pub use statement::Statement; use std::collections::HashMap; use time::OffsetDateTime; diff --git a/iam/src/policy/action.rs b/iam/src/policy/action.rs index 24e2d1353..eccbc8440 100644 --- a/iam/src/policy/action.rs +++ b/iam/src/policy/action.rs @@ -40,7 +40,7 @@ impl Validator for ActionSet { } } -#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, IntoStaticStr)] +#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone)] #[serde(try_from = "&str", untagged)] pub enum Action { S3Action(S3Action), @@ -55,11 +55,22 @@ impl Action { } } +impl From<&Action> for &str { + fn from(value: &Action) -> &'static str { + match value { + Action::S3Action(s) => s.into(), + Action::AdminAction(s) => s.into(), + Action::StsAction(s) => s.into(), + Action::KmsAction(s) => s.into(), + } + } +} + impl Action { const S3_PREFIX: &'static str = "s3:"; - const ADMIN_PREFIX: &str = "admin:"; - const STS_PREFIX: &str = "sts:"; - const KMS_PREFIX: &str = "kms:"; + const ADMIN_PREFIX: &'static str = "admin:"; + const STS_PREFIX: &'static str = "sts:"; + const KMS_PREFIX: &'static str = "kms:"; } impl TryFrom<&str> for Action { @@ -86,8 +97,10 @@ impl TryFrom<&str> for Action { } #[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, EnumString, IntoStaticStr)] +#[cfg_attr(test, derive(Default))] #[serde(try_from = "&str", into = "&str")] pub enum S3Action { + #[cfg_attr(test, default)] #[strum(serialize = "s3:*")] AllActions, #[strum(serialize = "s3:GetBucketLocation")] diff --git a/iam/src/policy/function.rs b/iam/src/policy/function.rs index b86ba19dc..1141373e1 100644 --- a/iam/src/policy/function.rs +++ b/iam/src/policy/function.rs @@ -1,7 +1,8 @@ -use std::{collections::HashMap, ops::Deref}; - -use func::Func; -use serde::{de, Deserialize, Serialize}; +use crate::policy::function::condition::Condition; +use serde::ser::SerializeMap; +use serde::{de, Deserialize, Serialize, Serializer}; +use std::collections::HashMap; +use std::collections::HashSet; pub mod addr; pub mod binary; @@ -14,12 +15,65 @@ pub mod key_name; pub mod number; pub mod string; -#[derive(Clone, Default, Serialize)] -pub struct Functions(pub Vec); +#[derive(Clone, Default)] +pub struct Functions { + for_any_value: Vec, + for_all_values: Vec, + for_normal: Vec, +} impl Functions { pub fn evaluate(&self, values: &HashMap>) -> bool { - self.0.iter().all(|x| x.evaluate(values)) + for c in self.for_any_value.iter() { + if !c.evaluate(false, values) { + return false; + } + } + + for c in self.for_all_values.iter() { + if !c.evaluate(true, values) { + return false; + } + } + + for c in self.for_normal.iter() { + if !c.evaluate(false, values) { + return false; + } + } + + true + } + + pub fn is_empty(&self) -> bool { + self.for_all_values.is_empty() && self.for_any_value.is_empty() && self.for_normal.is_empty() + } +} + +impl Serialize for Functions { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut se = + serializer.serialize_map(Some(self.for_any_value.len() + self.for_all_values.len() + self.for_normal.len()))?; + + for conditions in self.for_all_values.iter() { + se.serialize_key(format!("ForAllValues:{}", conditions.to_key()).as_str())?; + conditions.serialize_map(&mut se)?; + } + + for conditions in self.for_any_value.iter() { + se.serialize_key(format!("ForAnyValue:{}", conditions.to_key()).as_str())?; + conditions.serialize_map(&mut se)?; + } + + for conditions in self.for_normal.iter() { + se.serialize_key(conditions.to_key())?; + conditions.serialize_map(&mut se)?; + } + + se.end() } } @@ -44,30 +98,44 @@ impl<'de> Deserialize<'de> for Functions { { use serde::de::Error; - let inner_data = Vec::with_capacity(map.size_hint().unwrap_or(0)); - while let Some(key) = map.next_key::<&str>()? { - let mut tokens = key.split(":"); - let name = tokens.next(); - let qualifier = tokens.next(); + let mut hash = HashSet::with_capacity(map.size_hint().unwrap_or_default()); - // 多个: - if tokens.next().is_some() { - return Err(A::Error::custom("invalid codition")); + let mut inner_data = Functions::default(); + while let Some(key) = map.next_key::<&str>()? { + if hash.contains(&key) { + return Err(Error::custom(format!("duplicate condition operator `{}`", key))); } - let Some(_name) = name else { return Err(A::Error::custom("invalid codition")) }; + hash.insert(key); - let f = match qualifier { - Some("ForAnyValues") => Func::ForAnyValues, - Some("ForAllValues") => Func::ForAllValues, - Some(q) => return Err(A::Error::custom(format!("invalid qualifier `{q}`"))), - None => Func::ForNormal, - }; + let mut tokens = key.split(":"); + let mut qualifier = tokens.next(); + let mut name = tokens.next(); + if name.is_none() { + name = qualifier; + qualifier = None; + } - // inner_data.push(f(name.try_into()?)) + if tokens.next().is_some() { + return Err(Error::custom("invalid condition operator")); + } + + let Some(name) = name else { return Err(Error::custom("has no condition operator")) }; + + let condition = Condition::from_deserializer(name, &mut map)?; + match qualifier { + Some("ForAnyValue") => inner_data.for_any_value.push(condition), + Some("ForAllValues") => inner_data.for_all_values.push(condition), + Some(q) => return Err(Error::custom(format!("invalid qualifier `{q}`"))), + None => inner_data.for_normal.push(condition), + } } - Ok(Functions(inner_data)) + if inner_data.is_empty() { + return Err(Error::custom("has no condition element")); + } + + Ok(inner_data) } } @@ -75,21 +143,21 @@ impl<'de> Deserialize<'de> for Functions { } } -impl Deref for Functions { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - #[derive(Clone, Serialize, Deserialize)] pub struct Value; #[cfg(test)] mod tests { + use crate::policy::function::condition::Condition::*; + use crate::policy::function::func::FuncKeyValue; + use crate::policy::function::key::Key; + use crate::policy::function::key_name::KeyName; + use crate::policy::function::string::StringFunc; + use crate::policy::function::string::StringFuncValue; + use crate::policy::Functions; + use test_case::test_case; - #[test_case::test_case( + #[test_case( r#"{ "Null": { "s3:x-amz-server-side-encryption-customer-algorithm": true @@ -97,9 +165,9 @@ mod tests { "Null": { "s3:x-amz-server-side-encryption-customer-algorithm": "true" } - }"# => true; "1")] - #[test_case::test_case(r#"{}"# => true; "2")] - #[test_case::test_case( + }"# => false; "1")] + #[test_case(r#"{}"# => false; "2")] + #[test_case( r#"{ "StringLike": { "s3:x-amz-metadata-directive": "REPL*" @@ -117,7 +185,8 @@ mod tests { ] }, "StringNotLike": { - "s3:x-amz-storage-class": "STANDARD" + "s3:x-amz-storage-class": "STANDARD", + "s3:x-amz-server-side-encryption": "AES256" }, "Null": { "s3:x-amz-server-side-encryption-customer-algorithm": true @@ -130,7 +199,7 @@ mod tests { } }"# => true; "3" )] - #[test_case::test_case( + #[test_case( r#"{ "StringLike": { "s3:x-amz-metadata-directive": "REPL*" @@ -168,7 +237,144 @@ mod tests { } }"# => true; "4" )] - fn test_serde(input: &str) -> bool { - true + #[test_case( + r#"{ + "IpAddress": { + "aws:SourceIp": [ + "192.168.1.0/24" + ] + }, + "NotIpAddress": { + "aws:SourceIp": [ + "10.1.10.0/24" + ] + }, + "Null": { + "s3:x-amz-server-side-encryption-customer-algorithm": [ + true + ] + }, + "StringEquals": { + "s3:x-amz-copy-source": [ + "mybucket/myobject" + ] + }, + "StringLike": { + "s3:x-amz-metadata-directive": [ + "REPL*" + ] + }, + "StringNotEquals": { + "s3:x-amz-server-side-encryption": [ + "AES256" + ] + }, + "StringNotLike": { + "s3:x-amz-storage-class": [ + "STANDARD" + ] + } + }"# => true; + "5" + )] + #[test_case( + r#"{ + "IpAddress": { + "aws:SourceIp": [ + "192.168.1.0/24" + ] + }, + "NotIpAddress": { + "aws:SourceIp": [ + "10.1.10.0/24" + ] + }, + "Null": { + "s3:x-amz-server-side-encryption-customer-algorithm": [ + true + ] + }, + "StringEquals": { + "s3:x-amz-copy-source": [ + "mybucket/myobject" + ] + }, + "StringLike": { + "s3:x-amz-metadata-directive": [ + "REPL*" + ] + }, + "StringNotEquals": { + "s3:x-amz-server-side-encryption": [ + "aws:kms" + ] + }, + "StringNotLike": { + "s3:x-amz-storage-class": [ + "STANDARD" + ] + } + }"# => true; + "6" + )] + fn test_de(input: &str) -> bool { + serde_json::from_str::(input) + .map_err(|e| eprintln!("{e:?}")) + .is_ok() + } + + #[test_case( + Functions { + for_normal: vec![StringNotLike(StringFunc { + 0: vec![FuncKeyValue { + key: Key::try_from("s3:LocationConstraint").unwrap(), + values: StringFuncValue(vec!["us-east-1"].into_iter().map(ToOwned::to_owned).collect()), + }], + })], + ..Default::default() + }, + r#"{"StringNotLike":{"s3:LocationConstraint":"us-east-1"}}"#; + "1" + )] + #[test_case( + Functions { + for_all_values: vec![StringNotLike(StringFunc { + 0: vec![FuncKeyValue { + key: Key::try_from("s3:LocationConstraint").unwrap(), + values: StringFuncValue(vec!["us-east-1"].into_iter().map(ToOwned::to_owned).collect()), + }], + })], + ..Default::default() + }, + r#"{"ForAllValues:StringNotLike":{"s3:LocationConstraint":"us-east-1"}}"#; + "2" + )] + #[test_case( + Functions { + for_any_value: vec![StringNotLike(StringFunc { + 0: vec![FuncKeyValue { + key: Key::try_from("s3:LocationConstraint").unwrap(), + values: StringFuncValue(vec!["us-east-1", "us-east-2"].into_iter().map(ToOwned::to_owned).collect()), + }], + })], + for_all_values: vec![StringNotLike(StringFunc { + 0: vec![FuncKeyValue { + key: Key::try_from("s3:LocationConstraint").unwrap(), + values: StringFuncValue(vec!["us-east-1"].into_iter().map(ToOwned::to_owned).collect()), + }], + })], + for_normal: vec![StringNotLike(StringFunc { + 0: vec![FuncKeyValue { + key: Key::try_from("s3:LocationConstraint").unwrap(), + values: StringFuncValue(vec!["us-east-1"].into_iter().map(ToOwned::to_owned).collect()), + }], + })], + ..Default::default() + }, + r#"{"ForAllValues:StringNotLike":{"s3:LocationConstraint":"us-east-1"},"ForAnyValue:StringNotLike":{"s3:LocationConstraint":["us-east-1","us-east-2"]},"StringNotLike":{"s3:LocationConstraint":"us-east-1"}}"#; + "3" + )] + fn test_ser(input: Functions, expect: &str) { + assert_eq!(serde_json::to_string(&input).unwrap(), expect); } } diff --git a/iam/src/policy/function/addr.rs b/iam/src/policy/function/addr.rs index 36e083183..a141f2a69 100644 --- a/iam/src/policy/function/addr.rs +++ b/iam/src/policy/function/addr.rs @@ -7,16 +7,18 @@ pub type AddrFunc = InnerFunc; impl AddrFunc { pub(crate) fn evaluate(&self, values: &HashMap>) -> bool { - let rvalues = values.get(self.key.name().as_str()).map(|t| t.iter()).unwrap_or_default(); + for inner in self.0.iter() { + let rvalues = values.get(inner.key.name().as_str()).map(|t| t.iter()).unwrap_or_default(); - for r in rvalues { - let Ok(ip) = r.parse::() else { - return false; - }; + for r in rvalues { + let Ok(ip) = r.parse::() else { + return false; + }; - for ip_net in self.values.0.iter() { - if ip_net.contains(ip) { - return true; + for ip_net in inner.values.0.iter() { + if ip_net.contains(ip) { + return true; + } } } } @@ -47,7 +49,7 @@ impl<'de> Deserialize<'de> for AddrFuncValue { where E: serde::de::Error, { - Ok(AddrFuncValue(vec![Self::to_cidr::(v)?])) + Ok(AddrFuncValue(vec![Self::cidr::(v)?])) } fn visit_seq(self, mut seq: A) -> Result @@ -57,7 +59,7 @@ impl<'de> Deserialize<'de> for AddrFuncValue { Ok(AddrFuncValue({ let mut data = Vec::with_capacity(seq.size_hint().unwrap_or_default()); while let Some(v) = seq.next_element::<&str>()? { - data.push(Self::to_cidr::(v)?) + data.push(Self::cidr::(v)?) } data })) @@ -65,7 +67,7 @@ impl<'de> Deserialize<'de> for AddrFuncValue { } impl AddrFuncValueVisitor { - fn to_cidr(v: &str) -> Result { + fn cidr(v: &str) -> Result { let mut cidr_str = Cow::from(v); if v.find('/').is_none() { cidr_str.to_mut().push_str("/32"); @@ -84,6 +86,7 @@ impl<'de> Deserialize<'de> for AddrFuncValue { #[cfg(test)] mod tests { use super::{AddrFunc, AddrFuncValue}; + use crate::policy::function::func::FuncKeyValue; use crate::policy::function::{ key::Key, key_name::AwsKeyName::*, @@ -93,8 +96,10 @@ mod tests { fn new_func(name: KeyName, variable: Option, value: Vec<&str>) -> AddrFunc { AddrFunc { - key: Key { name, variable }, - values: AddrFuncValue(value.into_iter().map(|x| x.parse().unwrap()).collect()), + 0: vec![FuncKeyValue { + key: Key { name, variable }, + values: AddrFuncValue(value.into_iter().filter_map(|x| x.parse().ok()).collect()), + }], } } diff --git a/iam/src/policy/function/bool_null.rs b/iam/src/policy/function/bool_null.rs index b2ee1dbe2..e48e210eb 100644 --- a/iam/src/policy/function/bool_null.rs +++ b/iam/src/policy/function/bool_null.rs @@ -1,23 +1,34 @@ use super::func::InnerFunc; +use serde::de::{Error, IgnoredAny, SeqAccess}; use serde::{de, Deserialize, Deserializer, Serialize}; use std::{collections::HashMap, fmt}; pub type BoolFunc = InnerFunc; impl BoolFunc { pub fn evaluate_bool(&self, values: &HashMap>) -> bool { - match values.get(self.key.name().as_str()).and_then(|x| x.get(0)) { - Some(x) => self.values.0.to_string().as_str() == x, - None => false, + for inner in self.0.iter() { + if !match values.get(inner.key.name().as_str()).and_then(|x| x.get(0)) { + Some(x) => inner.values.0.to_string().as_str() == x, + None => false, + } { + return false; + } } + + true } pub fn evaluate_null(&self, values: &HashMap>) -> bool { - let len = values.get(self.key.name().as_str()).map(Vec::len).unwrap_or(0); - if self.values.0 { - return len == 0; + for inner in self.0.iter() { + let len = values.get(inner.key.name().as_str()).map(Vec::len).unwrap_or(0); + let r = if inner.values.0 { len == 0 } else { len != 0 }; + + if !r { + return false; + } } - len != 0 + true } } @@ -50,17 +61,32 @@ impl<'de> Deserialize<'de> for BoolFuncValue { fn visit_bool(self, value: bool) -> Result where - E: de::Error, + E: Error, { Ok(BoolFuncValue(value)) } fn visit_str(self, value: &str) -> Result where - E: de::Error, + E: Error, { Ok(BoolFuncValue(value.parse::().map_err(|e| E::custom(format!("{e:?}")))?)) } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let Some(v) = seq.next_element::()? else { + return Err(Error::custom("no value for boolean")); + }; + + if seq.next_element::()?.is_some() { + return Err(Error::custom("only allow one boolean value")); + } + + Ok(v) + } } deserializer.deserialize_any(BoolOrStringVisitor) @@ -70,6 +96,7 @@ impl<'de> Deserialize<'de> for BoolFuncValue { #[cfg(test)] mod tests { use super::{BoolFunc, BoolFuncValue}; + use crate::policy::function::func::FuncKeyValue; use crate::policy::function::{ key::Key, key_name::AwsKeyName::*, @@ -79,8 +106,10 @@ mod tests { fn new_func(name: KeyName, variable: Option, value: bool) -> BoolFunc { BoolFunc { - key: Key { name, variable }, - values: BoolFuncValue(value), + 0: vec![FuncKeyValue { + key: Key { name, variable }, + values: BoolFuncValue(value), + }], } } @@ -92,6 +121,8 @@ mod tests { #[test_case(r#"{"aws:SecureTransport/a": "false"}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), false); "10")] #[test_case(r#"{"aws:SecureTransport/a": true}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), true); "11")] #[test_case(r#"{"aws:SecureTransport/a": false}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), false); "12")] + #[test_case(r#"{"aws:SecureTransport/a": [true]}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), true); "13")] + #[test_case(r#"{"aws:SecureTransport/a": ["false"]}"#, new_func(Aws(AWSSecureTransport), Some("a".into()), false); "14")] fn test_deser(input: &str, expect: BoolFunc) -> Result<(), serde_json::Error> { let v: BoolFunc = serde_json::from_str(input)?; assert_eq!(v, expect); @@ -103,6 +134,9 @@ mod tests { #[test_case(r#"{"aws:usernamea/value":"johndoe"}"#)] #[test_case(r#"{"aws:usernamea/value":["johndoe", "aaa"]}"#)] #[test_case(r#""aaa""#)] + #[test_case(r#"{"aws:SecureTransport/a": ["false", "true"]}"#)] + #[test_case(r#"{"aws:SecureTransport/a": [true, false]}"#)] + #[test_case(r#"{"aws:SecureTransport/a": ["aa"]}"#)] fn test_deser_failed(input: &str) { assert!(serde_json::from_str::(input).is_err()); } @@ -111,6 +145,7 @@ mod tests { #[test_case(r#"{"aws:SecureTransport":"false"}"#, new_func(Aws(AWSSecureTransport), None, false);"2")] #[test_case(r#"{"aws:SecureTransport/aa":"true"}"#, new_func(Aws(AWSSecureTransport),Some("aa".into()), true);"3")] #[test_case(r#"{"aws:SecureTransport/aa":"false"}"#, new_func(Aws(AWSSecureTransport), Some("aa".into()), false);"4")] + # [test_case(r#"{"aws:SecureTransport/aa":"false"}"#, new_func(Aws(AWSSecureTransport), Some("aa".into()), false); "5")] fn test_ser(expect: &str, input: BoolFunc) -> Result<(), serde_json::Error> { let v = serde_json::to_string(&input)?; assert_eq!(v.as_str(), expect); diff --git a/iam/src/policy/function/condition.rs b/iam/src/policy/function/condition.rs index 4f743af31..7ce8e6168 100644 --- a/iam/src/policy/function/condition.rs +++ b/iam/src/policy/function/condition.rs @@ -1,11 +1,12 @@ +use serde::de::{Error, MapAccess}; +use serde::ser::SerializeMap; +use serde::{Deserialize, Serialize, Serializer}; use std::collections::HashMap; - -use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use super::{addr::AddrFunc, binary::BinaryFunc, bool_null::BoolFunc, date::DateFunc, number::NumberFunc, string::StringFunc}; -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Deserialize)] pub enum Condition { StringEquals(StringFunc), StringNotEquals(StringFunc), @@ -34,16 +35,73 @@ pub enum Condition { } impl Condition { + pub fn from_deserializer<'a, D: MapAccess<'a>>(key: &str, d: &mut D) -> Result { + Ok(match key { + "StringEquals" => Self::StringEquals(d.next_value()?), + "StringNotEquals" => Self::StringNotEquals(d.next_value()?), + "StringEqualsIgnoreCase" => Self::StringEqualsIgnoreCase(d.next_value()?), + "StringNotEqualsIgnoreCase" => Self::StringNotEqualsIgnoreCase(d.next_value()?), + "StringLike" => Self::StringLike(d.next_value()?), + "StringNotLike" => Self::StringNotLike(d.next_value()?), + "BinaryEquals" => Self::BinaryEquals(d.next_value()?), + "IpAddress" => Self::IpAddress(d.next_value()?), + "NotIpAddress" => Self::NotIpAddress(d.next_value()?), + "Null" => Self::Null(d.next_value()?), + "Bool" => Self::Bool(d.next_value()?), + "NumericEquals" => Self::NumericEquals(d.next_value()?), + "NumericNotEquals" => Self::NumericNotEquals(d.next_value()?), + "NumericLessThan" => Self::NumericLessThan(d.next_value()?), + "NumericGreaterThan" => Self::NumericGreaterThan(d.next_value()?), + "NumericGreaterThanIfExists" => Self::NumericGreaterThanIfExists(d.next_value()?), + "NumericGreaterThanEquals" => Self::NumericGreaterThanEquals(d.next_value()?), + "DateEquals" => Self::DateEquals(d.next_value()?), + "DateNotEquals" => Self::DateNotEquals(d.next_value()?), + "DateLessThanEquals" => Self::DateLessThanEquals(d.next_value()?), + "DateGreaterThan" => Self::DateGreaterThan(d.next_value()?), + "DateGreaterThanEquals" => Self::DateGreaterThanEquals(d.next_value()?), + _ => Err(Error::custom(format!("unknown key: {key}")))?, + }) + } + + pub fn to_key(&self) -> &'static str { + match self { + Condition::StringEquals(_) => "StringEquals", + Condition::StringNotEquals(_) => "StringNotEquals", + Condition::StringEqualsIgnoreCase(_) => "StringEqualsIgnoreCase", + Condition::StringNotEqualsIgnoreCase(_) => "StringNotEqualsIgnoreCase", + Condition::StringLike(_) => "StringLike", + Condition::StringNotLike(_) => "StringNotLike", + Condition::BinaryEquals(_) => "BinaryEquals", + Condition::IpAddress(_) => "IpAddress", + Condition::NotIpAddress(_) => "NotIpAddress", + Condition::Null(_) => "Null", + Condition::Bool(_) => "Bool", + Condition::NumericEquals(_) => "NumericEquals", + Condition::NumericNotEquals(_) => "NumericNotEquals", + Condition::NumericLessThan(_) => "NumericLessThan", + Condition::NumericLessThanEquals(_) => "NumericLessThanEquals", + Condition::NumericGreaterThan(_) => "NumericGreaterThan", + Condition::NumericGreaterThanIfExists(_) => "NumericGreaterThanIfExists", + Condition::NumericGreaterThanEquals(_) => "NumericGreaterThanEquals", + Condition::DateEquals(_) => "DateEquals", + Condition::DateNotEquals(_) => "DateNotEquals", + Condition::DateLessThan(_) => "DateLessThan", + Condition::DateLessThanEquals(_) => "DateLessThanEquals", + Condition::DateGreaterThan(_) => "DateGreaterThan", + Condition::DateGreaterThanEquals(_) => "DateGreaterThanEquals", + } + } + pub fn evaluate(&self, for_all: bool, values: &HashMap>) -> bool { use Condition::*; let r = match self { - StringEquals(s) => s.evaluate(for_all, false, false, values), - StringNotEquals(s) => s.evaluate(for_all, false, false, values), - StringEqualsIgnoreCase(s) => s.evaluate(for_all, true, false, values), - StringNotEqualsIgnoreCase(s) => s.evaluate(for_all, true, false, values), - StringLike(s) => s.evaluate(for_all, false, true, values), - StringNotLike(s) => s.evaluate(for_all, false, true, values), + StringEquals(s) => s.evaluate(for_all, false, false, false, values), + StringNotEquals(s) => s.evaluate(for_all, false, false, true, values), + StringEqualsIgnoreCase(s) => s.evaluate(for_all, true, false, false, values), + StringNotEqualsIgnoreCase(s) => s.evaluate(for_all, true, false, true, values), + StringLike(s) => s.evaluate(for_all, false, true, false, values), + StringNotLike(s) => s.evaluate(for_all, false, true, true, values), BinaryEquals(s) => todo!(), IpAddress(s) => s.evaluate(values), NotIpAddress(s) => s.evaluate(values), @@ -71,8 +129,38 @@ impl Condition { } } + #[inline] pub fn is_negate(&self) -> bool { use Condition::*; matches!(self, StringNotEquals(_) | StringNotEqualsIgnoreCase(_) | NotIpAddress(_)) } + + pub fn serialize_map(&self, se: &mut T) -> Result<(), T::Error> { + match self { + Condition::StringEquals(s) => se.serialize_value(s), + Condition::StringNotEquals(s) => se.serialize_value(s), + Condition::StringEqualsIgnoreCase(s) => se.serialize_value(s), + Condition::StringNotEqualsIgnoreCase(s) => se.serialize_value(s), + Condition::StringLike(s) => se.serialize_value(s), + Condition::StringNotLike(s) => se.serialize_value(s), + Condition::BinaryEquals(s) => se.serialize_value(s), + Condition::IpAddress(s) => se.serialize_value(s), + Condition::NotIpAddress(s) => se.serialize_value(s), + Condition::Null(s) => se.serialize_value(s), + Condition::Bool(s) => se.serialize_value(s), + Condition::NumericEquals(s) => se.serialize_value(s), + Condition::NumericNotEquals(s) => se.serialize_value(s), + Condition::NumericLessThan(s) => se.serialize_value(s), + Condition::NumericLessThanEquals(s) => se.serialize_value(s), + Condition::NumericGreaterThan(s) => se.serialize_value(s), + Condition::NumericGreaterThanIfExists(s) => se.serialize_value(s), + Condition::NumericGreaterThanEquals(s) => se.serialize_value(s), + Condition::DateEquals(s) => se.serialize_value(s), + Condition::DateNotEquals(s) => se.serialize_value(s), + Condition::DateLessThan(s) => se.serialize_value(s), + Condition::DateLessThanEquals(s) => se.serialize_value(s), + Condition::DateGreaterThan(s) => se.serialize_value(s), + Condition::DateGreaterThanEquals(s) => se.serialize_value(s), + } + } } diff --git a/iam/src/policy/function/date.rs b/iam/src/policy/function/date.rs index beb52e996..fa44536cb 100644 --- a/iam/src/policy/function/date.rs +++ b/iam/src/policy/function/date.rs @@ -6,21 +6,23 @@ use time::{format_description::well_known::Rfc3339, OffsetDateTime}; pub type DateFunc = InnerFunc; impl DateFunc { - pub fn evaluate( - &self, - op: impl FnOnce(&OffsetDateTime, &OffsetDateTime) -> bool, - values: &HashMap>, - ) -> bool { - let v = match values.get(self.key.name().as_str()).and_then(|x| x.get(0)) { - Some(x) => x, - None => return false, - }; + pub fn evaluate(&self, op: impl Fn(&OffsetDateTime, &OffsetDateTime) -> bool, values: &HashMap>) -> bool { + for inner in self.0.iter() { + let v = match values.get(inner.key.name().as_str()).and_then(|x| x.get(0)) { + Some(x) => x, + None => return false, + }; - let Ok(rv) = OffsetDateTime::parse(v, &Rfc3339) else { - return false; - }; + let Ok(rv) = OffsetDateTime::parse(v, &Rfc3339) else { + return false; + }; - op(&self.values.0, &rv) + if !op(&inner.values.0, &rv) { + return false; + } + } + + true } } @@ -74,6 +76,7 @@ impl<'de> Deserialize<'de> for DateFuncValue { #[cfg(test)] mod tests { use super::{DateFunc, DateFuncValue}; + use crate::policy::function::func::FuncKeyValue; use crate::policy::function::{ key::Key, key_name::KeyName::{self, *}, @@ -84,8 +87,10 @@ mod tests { fn new_func(name: KeyName, variable: Option, value: &str) -> DateFunc { DateFunc { - key: Key { name, variable }, - values: DateFuncValue(OffsetDateTime::parse(value, &Rfc3339).unwrap()), + 0: vec![FuncKeyValue { + key: Key { name, variable }, + values: DateFuncValue(OffsetDateTime::parse(value, &Rfc3339).unwrap()), + }], } } diff --git a/iam/src/policy/function/func.rs b/iam/src/policy/function/func.rs index 9c4e7bddc..2f75edca6 100644 --- a/iam/src/policy/function/func.rs +++ b/iam/src/policy/function/func.rs @@ -1,36 +1,22 @@ -use std::{collections::HashMap, marker::PhantomData}; +use std::marker::PhantomData; use serde::{ de::{self, Visitor}, Deserialize, Deserializer, Serialize, }; -use super::{condition::Condition, key::Key}; - -#[derive(Clone, Serialize, Deserialize)] -pub enum Func { - ForAnyValues(Vec), - ForAllValues(Vec), - ForNormal(Vec), -} - -impl Func { - pub fn evaluate(&self, values: &HashMap>) -> bool { - match self { - Self::ForAnyValues(conditions) => conditions.iter().all(|x| x.evaluate(true, values)), - Self::ForAllValues(conditions) => conditions.iter().all(|x| x.evaluate(false, values)), - Self::ForNormal(conditions) => conditions.iter().all(|x| x.evaluate(false, values)), - } - } -} +use super::key::Key; #[cfg_attr(test, derive(PartialEq, Eq, Debug))] -pub struct InnerFunc { +pub struct InnerFunc(pub(crate) Vec>); + +#[cfg_attr(test, derive(PartialEq, Eq, Debug))] +pub struct FuncKeyValue { pub key: Key, pub values: T, } -impl Clone for InnerFunc { +impl Clone for FuncKeyValue { fn clone(&self) -> Self { Self { key: self.key.clone(), @@ -39,6 +25,12 @@ impl Clone for InnerFunc { } } +impl Clone for InnerFunc { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + impl Serialize for InnerFunc { fn serialize(&self, serializer: S) -> Result where @@ -46,9 +38,13 @@ impl Serialize for InnerFunc { { use serde::ser::SerializeMap; - let mut map = serializer.serialize_map(Some(1))?; - map.serialize_key(&self.key)?; - map.serialize_value(&self.values)?; + let mut map = serializer.serialize_map(Some(self.0.len()))?; + + for kv in self.0.iter() { + map.serialize_key(&kv.key)?; + map.serialize_value(&kv.values)?; + } + map.end() } } @@ -78,11 +74,16 @@ where { use serde::de::Error; - let Some((key, values)) = map.next_entry::()? else { - return Err(A::Error::custom("no k-v pair")); - }; + let mut inner = Vec::with_capacity(map.size_hint().unwrap_or(0)); + while let Some((key, values)) = map.next_entry::()? { + inner.push(FuncKeyValue { key, values }); + } - Ok(InnerFunc { key, values }) + if inner.is_empty() { + return Err(Error::custom("has no condition key")); + } + + Ok(InnerFunc(inner)) } } diff --git a/iam/src/policy/function/key.rs b/iam/src/policy/function/key.rs index 54ba53a18..f5f224eef 100644 --- a/iam/src/policy/function/key.rs +++ b/iam/src/policy/function/key.rs @@ -1,7 +1,6 @@ -use serde::{Deserialize, Serialize}; - use super::key_name::KeyName; use crate::policy::{Error, Validator}; +use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq, Eq))] @@ -19,8 +18,8 @@ impl Key { self.name.eq(other) } - pub fn val_name(&self) -> String { - self.name.val_name() + pub fn var_name(&self) -> String { + self.name.var_name() } pub fn name(&self) -> String { @@ -34,7 +33,12 @@ impl Key { impl From for String { fn from(value: Key) -> Self { - value.name() + let mut data = String::from(Into::<&str>::into(&value.name)); + if let Some(x) = value.variable.as_ref() { + data.push('/'); + data.push_str(&x); + } + data } } diff --git a/iam/src/policy/function/key_name.rs b/iam/src/policy/function/key_name.rs index f2fb6fdbf..0ee33d95e 100644 --- a/iam/src/policy/function/key_name.rs +++ b/iam/src/policy/function/key_name.rs @@ -35,7 +35,7 @@ impl TryFrom<&str> for KeyName { } impl KeyName { - pub const COMMON_KEYS: &[KeyName] = &[ + pub const COMMON_KEYS: &'static [KeyName] = &[ // s3 KeyName::S3(S3KeyName::S3SignatureVersion), KeyName::S3(S3KeyName::S3AuthType), @@ -82,8 +82,36 @@ impl KeyName { KeyName::Jwt(JwtKeyName::JWTClientID), ]; - pub fn name(&self) -> &str { + pub const fn prefix(&self) -> usize { match self { + KeyName::Aws(_) => "aws:".len(), + KeyName::Jwt(_) => "jwt:".len(), + KeyName::Ldap(_) => "ldap:".len(), + KeyName::Sts(_) => "sts:".len(), + KeyName::Svc(_) => "svc:".len(), + KeyName::S3(_) => "s3:".len(), + } + } + + pub fn name(&self) -> &str { + &Into::<&str>::into(self)[self.prefix()..] + } + + pub fn var_name(&self) -> String { + match self { + KeyName::Aws(s) => format!("${{aws:{}}}", Into::<&str>::into(s)), + KeyName::Jwt(s) => format!("${{jwt:{}}}", Into::<&str>::into(s)), + KeyName::Ldap(s) => format!("${{ldap:{}}}", Into::<&str>::into(s)), + KeyName::Sts(s) => format!("${{sts:{}}}", Into::<&str>::into(s)), + KeyName::Svc(s) => format!("${{svc:{}}}", Into::<&str>::into(s)), + KeyName::S3(s) => format!("${{s3:{}}}", Into::<&str>::into(s)), + } + } +} + +impl From<&KeyName> for &'static str { + fn from(k: &KeyName) -> Self { + match k { KeyName::Aws(aws) => aws.into(), KeyName::Jwt(jwt) => jwt.into(), KeyName::Ldap(ldap) => ldap.into(), @@ -92,17 +120,6 @@ impl KeyName { KeyName::S3(s3) => s3.into(), } } - - pub fn val_name(&self) -> String { - match self { - KeyName::Aws(aws) => Into::<&str>::into(aws).to_owned(), - KeyName::Jwt(jwt) => Into::<&str>::into(jwt).to_owned(), - KeyName::Ldap(ldap) => Into::<&str>::into(ldap).to_owned(), - KeyName::Sts(sts) => Into::<&str>::into(sts).to_owned(), - KeyName::Svc(svc) => Into::<&str>::into(svc).to_owned(), - KeyName::S3(s3) => Into::<&str>::into(s3).to_owned(), - } - } } #[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)] @@ -137,6 +154,21 @@ pub enum S3KeyName { #[strum(serialize = "s3:max-keys")] S3MaxKeys, + + #[strum(serialize = "s3:x-amz-metadata-directive")] + S3XAmzMetadataDirective, + + #[strum(serialize = "s3:x-amz-storage-class")] + S3XAmzStorageClass, + + #[strum(serialize = "s3:prefix")] + S3Prefix, + + #[strum(serialize = "s3:delimiter")] + S3Delimiter, + + #[strum(serialize = "s3:ExistingObjectTag")] + S3ExistingObjectTag, } #[derive(Clone, EnumString, Debug, IntoStaticStr, Eq, PartialEq, Serialize, Deserialize)] diff --git a/iam/src/policy/function/number.rs b/iam/src/policy/function/number.rs index fe91e2133..4633c04b1 100644 --- a/iam/src/policy/function/number.rs +++ b/iam/src/policy/function/number.rs @@ -13,17 +13,23 @@ pub type NumberFunc = InnerFunc; pub struct NumberFuncValue(i64); impl NumberFunc { - pub fn evaluate(&self, op: impl FnOnce(&i64, &i64) -> bool, if_exists: bool, values: &HashMap>) -> bool { - let v = match values.get(self.key.name().as_str()).and_then(|x| x.get(0)) { - Some(x) => x, - None => return if_exists, - }; + pub fn evaluate(&self, op: impl Fn(&i64, &i64) -> bool, if_exists: bool, values: &HashMap>) -> bool { + for inner in self.0.iter() { + let v = match values.get(inner.key.name().as_str()).and_then(|x| x.get(0)) { + Some(x) => x, + None => return if_exists, + }; - let Ok(rv) = v.parse::() else { - return false; - }; + let Ok(rv) = v.parse::() else { + return false; + }; - op(&rv, &self.values.0) + if !op(&rv, &inner.values.0) { + return false; + } + } + + true } } @@ -50,13 +56,6 @@ impl<'de> Deserialize<'de> for NumberFuncValue { formatter.write_str("a number or a string that can be represented as a number.") } - fn visit_str(self, value: &str) -> Result - where - E: Error, - { - Ok(NumberFuncValue(value.parse().map_err(|e| E::custom(format!("{e:?}")))?)) - } - fn visit_i64(self, value: i64) -> Result where E: Error, @@ -70,6 +69,13 @@ impl<'de> Deserialize<'de> for NumberFuncValue { { Ok(NumberFuncValue(value as i64)) } + + fn visit_str(self, value: &str) -> Result + where + E: Error, + { + Ok(NumberFuncValue(value.parse().map_err(|e| E::custom(format!("{e:?}")))?)) + } } deserializer.deserialize_any(NumberVisitor) @@ -79,6 +85,7 @@ impl<'de> Deserialize<'de> for NumberFuncValue { #[cfg(test)] mod tests { use super::{NumberFunc, NumberFuncValue}; + use crate::policy::function::func::FuncKeyValue; use crate::policy::function::{ key::Key, key_name::KeyName::{self, *}, @@ -88,8 +95,10 @@ mod tests { fn new_func(name: KeyName, variable: Option, value: i64) -> NumberFunc { NumberFunc { - key: Key { name, variable }, - values: NumberFuncValue(value), + 0: vec![FuncKeyValue { + key: Key { name, variable }, + values: NumberFuncValue(value), + }], } } diff --git a/iam/src/policy/function/string.rs b/iam/src/policy/function/string.rs index 12602d833..a32274e6c 100644 --- a/iam/src/policy/function/string.rs +++ b/iam/src/policy/function/string.rs @@ -5,17 +5,43 @@ use std::collections::HashSet as Set; use std::fmt; use std::{borrow::Cow, collections::HashMap}; -use serde::{de, ser::SerializeSeq, Deserialize, Deserializer, Serialize}; - +use crate::policy::function::func::FuncKeyValue; use crate::policy::utils::wildcard; +use serde::{de, ser::SerializeSeq, Deserialize, Deserializer, Serialize}; use super::{func::InnerFunc, key_name::KeyName}; pub type StringFunc = InnerFunc; impl StringFunc { + pub(crate) fn evaluate( + &self, + for_all: bool, + ignore_case: bool, + like: bool, + negate: bool, + values: &HashMap>, + ) -> bool { + for inner in self.0.iter() { + let result = if like { + inner.eval_like(for_all, values) ^ negate + } else { + inner.eval(for_all, ignore_case, values) ^ negate + }; + + if !result { + return false; + } + } + + true + } +} + +impl FuncKeyValue { fn eval(&self, for_all: bool, ignore_case: bool, values: &HashMap>) -> bool { let rvalues = values + // http.CanonicalHeaderKey ? .get(self.key.name().as_str()) .map(|t| { t.iter() @@ -38,7 +64,7 @@ impl StringFunc { let mut c = Cow::from(c); for key in KeyName::COMMON_KEYS { match values.get(key.name()).and_then(|x| x.get(0)) { - Some(v) if !v.is_empty() => return Cow::Owned(c.to_mut().replace(key.name(), v)), + Some(v) if !v.is_empty() => return Cow::Owned(c.to_mut().replace(&key.var_name(), v)), _ => continue, }; } @@ -49,6 +75,7 @@ impl StringFunc { .collect::>(); let ivalues = rvalues.intersection(&fvalues); + if for_all { rvalues.is_empty() || rvalues.len() == ivalues.count() } else { @@ -67,7 +94,7 @@ impl StringFunc { let mut c = Cow::from(c); for key in KeyName::COMMON_KEYS { match values.get(key.name()).and_then(|x| x.get(0)) { - Some(v) if !v.is_empty() => return Cow::Owned(c.to_mut().replace(key.name(), v)), + Some(v) if !v.is_empty() => return Cow::Owned(c.to_mut().replace(&key.var_name(), v)), _ => continue, }; } @@ -88,20 +115,12 @@ impl StringFunc { for_all } - - pub(crate) fn evaluate(&self, for_all: bool, ignore_case: bool, like: bool, values: &HashMap>) -> bool { - if like { - self.eval_like(for_all, values) - } else { - self.eval(for_all, ignore_case, values) - } - } } /// 解析values字段 #[derive(Clone)] #[cfg_attr(test, derive(PartialEq, Eq, Debug))] -pub struct StringFuncValue(Set); +pub struct StringFuncValue(pub Set); impl Serialize for StringFuncValue { fn serialize(&self, serializer: S) -> Result @@ -165,7 +184,7 @@ impl<'d> Deserialize<'d> for StringFuncValue { if result.0.is_empty() { use serde::de::Error; - return Err(D::Error::custom("empty")); + return Err(Error::custom("empty")); } Ok(result) @@ -175,24 +194,34 @@ impl<'d> Deserialize<'d> for StringFuncValue { #[cfg(test)] mod tests { use super::{StringFunc, StringFuncValue}; + use crate::policy::function::func::FuncKeyValue; use crate::policy::function::{ key::Key, key_name::AwsKeyName::*, key_name::KeyName::{self, *}, }; + + use crate::policy::function::key_name::S3KeyName::S3LocationConstraint; use test_case::test_case; fn new_func(name: KeyName, variable: Option, values: Vec<&str>) -> StringFunc { StringFunc { - key: Key { name, variable }, - values: StringFuncValue(values.into_iter().map(|x| x.to_owned()).collect()), + 0: vec![FuncKeyValue { + key: Key { name, variable }, + values: StringFuncValue(values.into_iter().map(|x| x.to_owned()).collect()), + }], } } - #[test_case(r#"{"aws:username": "johndoe"}"#, new_func(Aws(AWSUsername), None, vec!["johndoe"]))] - #[test_case(r#"{"aws:username": ["johndoe", "aaa"]}"#, new_func(Aws(AWSUsername), None, vec!["johndoe", "aaa"]))] - #[test_case(r#"{"aws:username/value": "johndoe"}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe"]))] - #[test_case(r#"{"aws:username/value": ["johndoe", "aaa"]}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe", "aaa"]))] + #[test_case(r#"{"aws:username": "johndoe"}"#, + new_func(Aws(AWSUsername), None, vec!["johndoe"]) + )] + #[test_case(r#"{"aws:username": ["johndoe", "aaa"]}"#, new_func(Aws(AWSUsername), None, vec!["johndoe", "aaa"] + ))] + #[test_case(r#"{"aws:username/value": "johndoe"}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe"] + ))] + #[test_case(r#"{"aws:username/value": ["johndoe", "aaa"]}"#, new_func(Aws(AWSUsername), Some("value".into()), vec!["johndoe", "aaa"] + ))] fn test_deser(input: &str, expect: StringFunc) -> Result<(), serde_json::Error> { let v: StringFunc = serde_json::from_str(input)?; assert_eq!(v, expect); @@ -217,4 +246,178 @@ mod tests { assert_eq!(v.as_str(), expect); Ok(()) } + + fn new_fkv(name: &str, values: Vec<&str>) -> FuncKeyValue { + FuncKeyValue { + key: name.try_into().unwrap(), + values: StringFuncValue(values.into_iter().map(ToOwned::to_owned).collect()), + } + } + + fn test_eval( + s: FuncKeyValue, + for_all: bool, + ignore_case: bool, + negate: bool, + values: Vec<(&str, Vec<&str>)>, + ) -> bool { + let result = s.eval( + for_all, + ignore_case, + &values + .into_iter() + .map(|(k, v)| (k.to_owned(), v.into_iter().map(ToOwned::to_owned).collect::>())) + .collect(), + ); + + result ^ negate + } + + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => true ; "1")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => false ; "2")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![] => false ; "3")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("delimiter", vec!["/"])] => false ; "4")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => true ; "5")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => true ; "6")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => false ; "7")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![] => false ; "8")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("delimiter", vec!["/"])] => false ; "9")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("groups", vec!["prod", "art"])] => true ; "10")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("groups", vec!["art"])] => true ; "11")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![] => true ; "12")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("delimiter", vec!["/"])] => true ; "13")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("groups", vec!["prod", "art"])] => true ; "14")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("groups", vec!["art"])] => true ; "15")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![] => false ; "16")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("delimiter", vec!["/"])] => false ; "17")] + #[test_case(new_fkv("s3:LocationConstraint", vec![KeyName::S3(S3LocationConstraint).var_name().as_str()]), false, vec![("LocationConstraint", vec!["us-west-1"])] => true ; "18")] + #[test_case(new_fkv("s3:ExistingObjectTag/security", vec!["public"]), false, vec![("ExistingObjectTag/security", vec!["public"])] => true ; "19")] + #[test_case(new_fkv("s3:ExistingObjectTag/security", vec!["public"]), false, vec![("ExistingObjectTag/security", vec!["private"])] => false ; "20")] + #[test_case(new_fkv("s3:ExistingObjectTag/security", vec!["public"]), false, vec![("ExistingObjectTag/project", vec!["foo"])] => false ; "21")] + fn test_string_equals(s: FuncKeyValue, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool { + test_eval(s, for_all, false, false, values) + } + + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => false ; "1")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => true ; "2")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![] => true ; "3")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("delimiter", vec!["/"])] => true ; "4")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => false ; "5")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => false ; "6")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => true ; "7")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![] => true ; "8")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-1", "ap-southeast-1"]), false, vec![("delimiter", vec!["/"])] => true ; "9")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("groups", vec!["prod", "art"])] => false ; "10")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("groups", vec!["art"])] => false ; "11")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![] => false ; "12")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), true, vec![("delimiter", vec!["/"])] => false ; "13")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("groups", vec!["prod", "art"])] => false ; "14")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("groups", vec!["art"])] => false ; "15")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![] => true ; "16")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art"]), false, vec![("delimiter", vec!["/"])] => true ; "17")] + fn test_string_not_equals(s: FuncKeyValue, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool { + test_eval(s, for_all, false, true, values) + } + + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => true ; "1")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => false ; "2")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![] => false ; "3")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("delimiter", vec!["/"])] => false ; "4")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => true ; "5")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => true ; "6")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => false ; "7")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![] => false ; "8")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("delimiter", vec!["/"])] => false ; "9")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("groups", vec!["prod", "art"])] => true ; "10")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("groups", vec!["art"])] => true ; "11")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![] => true ; "12")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("delimiter", vec!["/"])] => true ; "13")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("groups", vec!["prod", "art"])] => true ; "14")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("groups", vec!["art"])] => true ; "15")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![] => false ; "16")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("delimiter", vec!["/"])] => false ; "17")] + fn test_string_equals_ignore_case(s: FuncKeyValue, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool { + test_eval(s, for_all, true, false, values) + } + + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => false ; "1")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => true ; "2")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![] => true ; "3")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/MYOBJECT"]), false, vec![("delimiter", vec!["/"])] => true ; "4")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => false ; "5")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => false ; "6")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => true ; "7")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![] => true ; "8")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["EU-WEST-1", "AP-southeast-1"]), false, vec![("delimiter", vec!["/"])] => true ; "9")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("groups", vec!["prod", "art"])] => false ; "10")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("groups", vec!["art"])] => false ; "11")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![] => false ; "12")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), true, vec![("delimiter", vec!["/"])] => false ; "13")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("groups", vec!["prod", "art"])] => false ; "14")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("groups", vec!["art"])] => false ; "15")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![] => true ; "16")] + #[test_case(new_fkv("jwt:groups", vec!["Prod", "Art"]), false, vec![("delimiter", vec!["/"])] => true ; "17")] + fn test_string_not_equals_ignore_case( + s: FuncKeyValue, + for_all: bool, + values: Vec<(&str, Vec<&str>)>, + ) -> bool { + test_eval(s, for_all, true, true, values) + } + + fn test_eval_like(s: FuncKeyValue, for_all: bool, negate: bool, values: Vec<(&str, Vec<&str>)>) -> bool { + let result = s.eval_like( + for_all, + &values + .into_iter() + .map(|(k, v)| (k.to_owned(), v.into_iter().map(ToOwned::to_owned).collect::>())) + .collect(), + ); + + result ^ negate + } + + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => true ; "1")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => false ; "2")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![] => false ; "3")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("delimiter", vec!["/"])] => false ; "4")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => true ; "5")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => true ; "6")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => false ; "7")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![] => false ; "8")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("delimiter", vec!["/"])] => false ; "9")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-2"])] => true ; "10")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("groups", vec!["prod", "art"])] => true ; "11")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("groups", vec!["art"])] => true ; "12")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![] => true ; "13")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("delimiter", vec!["/"])] => true ; "14")] + #[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("groups", vec!["prod", "art"])] => true ; "15")] + #[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("groups", vec!["art"])] => true ; "16")] + #[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![] => false ; "17")] + #[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("delimiter", vec!["/"])] => false ; "18")] + fn test_string_like(s: FuncKeyValue, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool { + test_eval_like(s, for_all, false, values) + } + + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["mybucket/myobject"])] => false ; "1")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("x-amz-copy-source", vec!["yourbucket/myobject"])] => true ; "2")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![] => true ; "3")] + #[test_case(new_fkv("s3:x-amz-copy-source", vec!["mybucket/myobject"]), false, vec![("delimiter", vec!["/"])] => true ; "4")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-1"])] => false ; "5")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["ap-southeast-1"])] => false ; "6")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["us-east-1"])] => true ; "7")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![] => true ; "8")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("delimiter", vec!["/"])] => true ; "9")] + #[test_case(new_fkv("s3:LocationConstraint", vec!["eu-west-*", "ap-southeast-1"]), false, vec![("LocationConstraint", vec!["eu-west-2"])] => false ; "10")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("groups", vec!["prod", "art"])] => false ; "11")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("groups", vec!["art"])] => false ; "12")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![] => false ; "13")] + #[test_case(new_fkv("jwt:groups", vec!["prod", "art*"]), true, vec![("delimiter", vec!["/"])] => false ; "14")] + #[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("groups", vec!["prod", "art"])] => false ; "15")] + #[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("groups", vec!["art"])] => false ; "16")] + #[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![] => true ; "17")] + #[test_case(new_fkv("jwt:groups", vec!["prod*", "art"]), false, vec![("delimiter", vec!["/"])] => true ; "18")] + fn test_string_not_like(s: FuncKeyValue, for_all: bool, values: Vec<(&str, Vec<&str>)>) -> bool { + test_eval_like(s, for_all, true, values) + } } diff --git a/iam/src/policy/policy.rs b/iam/src/policy/policy.rs index 28340c887..806ca387d 100644 --- a/iam/src/policy/policy.rs +++ b/iam/src/policy/policy.rs @@ -23,7 +23,7 @@ impl Policy { for statement in self.statements.iter().filter(|s| matches!(s.effect, Effect::Allow)) { if statement.is_allowed(args) { - return false; + return true; } } @@ -72,12 +72,12 @@ pub mod default { hash_set }), not_actions: ActionSet(Default::default()), - resoures: ResourceSet({ + resources: ResourceSet({ let mut hash_set = HashSet::new(); hash_set.insert(Resource::S3("*".into())); hash_set }), - conditions: Functions(vec![]), + conditions: Functions::default(), }], }, ), @@ -96,12 +96,12 @@ pub mod default { hash_set }), not_actions: ActionSet(Default::default()), - resoures: ResourceSet({ + resources: ResourceSet({ let mut hash_set = HashSet::new(); hash_set.insert(Resource::S3("*".into())); hash_set }), - conditions: Functions(vec![]), + conditions: Functions::default(), }], }, ), @@ -119,12 +119,12 @@ pub mod default { hash_set }), not_actions: ActionSet(Default::default()), - resoures: ResourceSet({ + resources: ResourceSet({ let mut hash_set = HashSet::new(); hash_set.insert(Resource::S3("*".into())); hash_set }), - conditions: Functions(vec![]), + conditions: Functions::default(), }], }, ), @@ -142,12 +142,12 @@ pub mod default { hash_set }), not_actions: ActionSet(Default::default()), - resoures: ResourceSet({ + resources: ResourceSet({ let mut hash_set = HashSet::new(); hash_set.insert(Resource::S3("*".into())); hash_set }), - conditions: Functions(vec![]), + conditions: Functions::default(), }], }, ), @@ -172,12 +172,12 @@ pub mod default { hash_set }), not_actions: ActionSet(Default::default()), - resoures: ResourceSet({ + resources: ResourceSet({ let mut hash_set = HashSet::new(); hash_set.insert(Resource::S3("*".into())); hash_set }), - conditions: Functions(vec![]), + conditions: Functions::default(), }], }, ), @@ -196,8 +196,8 @@ pub mod default { hash_set }), not_actions: ActionSet(Default::default()), - resoures: ResourceSet(HashSet::new()), - conditions: Functions(vec![]), + resources: ResourceSet(HashSet::new()), + conditions: Functions::default(), }, Statement { sid: "".into(), @@ -208,8 +208,8 @@ pub mod default { hash_set }), not_actions: ActionSet(Default::default()), - resoures: ResourceSet(HashSet::new()), - conditions: Functions(vec![]), + resources: ResourceSet(HashSet::new()), + conditions: Functions::default(), }, Statement { sid: "".into(), @@ -220,12 +220,12 @@ pub mod default { hash_set }), not_actions: ActionSet(Default::default()), - resoures: ResourceSet({ + resources: ResourceSet({ let mut hash_set = HashSet::new(); hash_set.insert(Resource::S3("*".into())); hash_set }), - conditions: Functions(vec![]), + conditions: Functions::default(), }, ], }, diff --git a/iam/src/policy/resource.rs b/iam/src/policy/resource.rs index 91c774973..5ae654697 100644 --- a/iam/src/policy/resource.rs +++ b/iam/src/policy/resource.rs @@ -52,19 +52,18 @@ pub enum Resource { } impl Resource { - pub const S3_PREFIX: &str = "arn:aws:s3:::"; + pub const S3_PREFIX: &'static str = "arn:aws:s3:::"; pub fn is_match(&self, resource: &str, conditons: &HashMap>) -> bool { let mut pattern = match self { Resource::S3(s) => s.to_owned(), Resource::Kms(s) => s.to_owned(), }; - if !conditons.is_empty() { for key in KeyName::COMMON_KEYS { if let Some(rvalue) = conditons.get(key.name()) { if matches!(rvalue.first().map(|c| !c.is_empty()), Some(true)) { - pattern = pattern.replace(key.name(), &rvalue[0]); + pattern = pattern.replace(&key.var_name(), &rvalue[0]); } } } @@ -83,7 +82,7 @@ impl TryFrom<&str> for Resource { type Error = Error; fn try_from(value: &str) -> Result { let resource = if value.starts_with(Self::S3_PREFIX) { - Resource::S3(value[Self::S3_PREFIX.len() + 1..].into()) + Resource::S3(value[Self::S3_PREFIX.len()..].into()) } else { return Err(Error::InvalidResource("unknown".into(), value.into())); }; @@ -116,3 +115,30 @@ impl Validator for Resource { Ok(()) } } + +#[cfg(test)] +mod tests { + use crate::policy::resource::Resource; + use std::collections::HashMap; + use test_case::test_case; + + #[test_case("arn:aws:s3:::*","mybucket" => true; "1")] + #[test_case("arn:aws:s3:::*","mybucket/myobject" => true; "2")] + #[test_case("arn:aws:s3:::mybucket*","mybucket" => true; "3")] + #[test_case("arn:aws:s3:::mybucket*","mybucket/myobject" => true; "4")] + #[test_case("arn:aws:s3:::*/*","mybucket/myobject"=> true; "5")] + #[test_case("arn:aws:s3:::mybucket/*","mybucket/myobject" => true; "6")] + #[test_case("arn:aws:s3:::mybucket*/myobject","mybucket/myobject" => true; "7")] + #[test_case("arn:aws:s3:::mybucket*/myobject","mybucket100/myobject" => true; "8")] + #[test_case("arn:aws:s3:::mybucket?0/2010/photos/*","mybucket20/2010/photos/1.jpg" => true; "9")] + #[test_case("arn:aws:s3:::mybucket","mybucket" => true; "10")] + #[test_case("arn:aws:s3:::mybucket?0","mybucket30" => true; "11")] + #[test_case("arn:aws:s3:::*/*","mybucket" => false; "12")] + #[test_case("arn:aws:s3:::mybucket/*","mybucket10/myobject" => false; "13")] + #[test_case("arn:aws:s3:::mybucket?0/2010/photos/*","mybucket0/2010/photos/1.jpg" => false; "14")] + #[test_case("arn:aws:s3:::mybucket","mybucket/myobject" => false; "15")] + fn test_resource_is_match(resource: &str, object: &str) -> bool { + let resource: Resource = resource.try_into().unwrap(); + resource.is_match(object, &HashMap::new()) + } +} diff --git a/iam/src/policy/statement.rs b/iam/src/policy/statement.rs index f10e7b8aa..481a35479 100644 --- a/iam/src/policy/statement.rs +++ b/iam/src/policy/statement.rs @@ -8,7 +8,7 @@ pub struct Statement { pub effect: Effect, pub actions: ActionSet, pub not_actions: ActionSet, - pub resoures: ResourceSet, + pub resources: ResourceSet, pub conditions: Functions, } @@ -61,12 +61,12 @@ impl Statement { } if self.is_kms() { - if resource == "/" || self.resoures.is_empty() { + if resource == "/" || self.resources.is_empty() { break 'c self.conditions.evaluate(&args.conditions); } } - if !self.resoures.is_match(&resource, &args.conditions) && !self.is_admin() && !self.is_sts() { + if !self.resources.is_match(&resource, &args.conditions) && !self.is_admin() && !self.is_sts() { break 'c false; } @@ -87,13 +87,13 @@ impl Validator for Statement { return Err(Error::NonAction); } - if self.resoures.is_empty() { + if self.resources.is_empty() { return Err(Error::NonResource); } self.actions.is_valid()?; self.not_actions.is_valid()?; - self.resoures.is_valid()?; + self.resources.is_valid()?; Ok(()) } diff --git a/iam/src/policy/utils/wildcard.rs b/iam/src/policy/utils/wildcard.rs index 5e0070aa4..b4451c9f1 100644 --- a/iam/src/policy/utils/wildcard.rs +++ b/iam/src/policy/utils/wildcard.rs @@ -38,6 +38,7 @@ where text.as_ref().len() <= pattern.as_ref().len() } +#[inline] fn inner_match(pattern: impl AsRef, name: impl AsRef, simple: bool) -> bool { let (pattern, name) = (pattern.as_ref(), name.as_ref()); @@ -49,10 +50,10 @@ fn inner_match(pattern: impl AsRef, name: impl AsRef, simple: bool) -> return true; } - deep_match(name.as_bytes(), pattern.as_bytes(), simple) + deep_match(pattern.as_bytes(), name.as_bytes(), simple) } -fn deep_match(mut name: &[u8], mut pattern: &[u8], simple: bool) -> bool { +fn deep_match(mut pattern: &[u8], mut name: &[u8], simple: bool) -> bool { while !pattern.is_empty() { match pattern[0] { b'?' => { @@ -63,8 +64,8 @@ fn deep_match(mut name: &[u8], mut pattern: &[u8], simple: bool) -> bool { b'*' => { return pattern.len() == 1 - || deep_match(name, &pattern[1..], simple) - || (!name.is_empty() && deep_match(&name[1..], pattern, simple)); + || deep_match(&pattern[1..], name, simple) + || (!name.is_empty() && deep_match(pattern, &name[1..], simple)); } _ => { @@ -137,6 +138,7 @@ mod tests { #[test_case::test_case("my-bucket/mnop*?and", "my-bucket/mnopqanda" => false; "50")] #[test_case::test_case("my-?-bucket/abc*", "my-bucket/mnopqanda" => false; "51")] #[test_case::test_case("a?", "a" => false; "52")] + #[test_case::test_case("*", "mybucket/myobject" => true; "53")] fn test_is_match(pattern: &str, text: &str) -> bool { is_match(pattern, text) } diff --git a/iam/src/store/object.rs b/iam/src/store/object.rs index 57c00ec26..b4a94fb6e 100644 --- a/iam/src/store/object.rs +++ b/iam/src/store/object.rs @@ -9,7 +9,6 @@ use ecstore::{ use futures::future::try_join_all; use log::{debug, warn}; use serde::{de::DeserializeOwned, Serialize}; -use tracing::error; use super::Store; use crate::{ diff --git a/iam/tests/policy_is_allowed.rs b/iam/tests/policy_is_allowed.rs new file mode 100644 index 000000000..84144caf4 --- /dev/null +++ b/iam/tests/policy_is_allowed.rs @@ -0,0 +1,615 @@ +use iam::policy::action::Action; +use iam::policy::action::ActionSet; +use iam::policy::action::S3Action::*; +use iam::policy::resource::ResourceSet; +use iam::policy::Effect::*; +use iam::policy::{Args, Policy, Statement, DEFAULT_VERSION}; +use serde_json::Value; +use std::collections::HashMap; +use test_case::test_case; + +#[derive(Default)] +struct ArgsBuilder { + pub account: String, + pub groups: Vec, + pub action: String, + pub bucket: String, + pub conditions: HashMap>, + pub is_owner: bool, + pub object: String, + pub claims: HashMap, + pub deny_only: bool, +} + +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(PutObjectAction), Action::S3Action(GetBucketLocationAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::*".try_into().unwrap()].into_iter().collect()), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetBucketLocation".into(), + bucket: "mybucket".into(), + ..Default::default() + } => true; + "1" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(PutObjectAction), Action::S3Action(GetBucketLocationAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::*".try_into().unwrap()].into_iter().collect()), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:PutObject".into(), + bucket: "mybucket".into(), + conditions: { + let mut h = HashMap::new(); + h.insert("x-amz-copy-source".into(), vec!["mybucket/myobject".into()]); + h.insert("SourceIp".into(), vec!["192.168.1.10".into()]); + h + }, + ..Default::default() + } => true; + "2" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(PutObjectAction), Action::S3Action(GetBucketLocationAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::*".try_into().unwrap()].into_iter().collect()), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + ..Default::default() + } => false; + "3" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(PutObjectAction), Action::S3Action(GetBucketLocationAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::*".try_into().unwrap()].into_iter().collect()), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetBucketLocation".into(), + bucket: "mybucket".into(), + ..Default::default() + } => true; + "4" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(PutObjectAction), Action::S3Action(GetBucketLocationAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::*".try_into().unwrap()].into_iter().collect()), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:PutObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + conditions: { + let mut h = HashMap::new(); + h.insert("x-amz-copy-source".into(), vec!["mybucket/myobject".into()]); + h.insert("SourceIp".into(), vec!["192.168.1.10".into()]); + h + }, + ..Default::default() + } => true; + "5" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(PutObjectAction), Action::S3Action(GetBucketLocationAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::*".try_into().unwrap()].into_iter().collect()), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + ..Default::default() + } => false; + "6" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetBucketLocation".into(), + bucket: "mybucket".into(), + ..Default::default() + } => false; + "7" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:PutObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + conditions: { + let mut h = HashMap::new(); + h.insert("x-amz-copy-source".into(), vec!["mybucket/myobject".into()]); + h.insert("SourceIp".into(), vec!["192.168.1.10".into()]); + h + }, + ..Default::default() + } => true; + "8" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + ..Default::default() + } => true; + "9" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetBucketLocation".into(), + bucket: "mybucket".into(), + ..Default::default() + } => false; + "10" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:PutObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + conditions: { + let mut h = HashMap::new(); + h.insert("x-amz-copy-source".into(), vec!["mybucket/myobject".into()]); + h.insert("SourceIp".into(), vec!["192.168.1.10".into()]); + h + }, + ..Default::default() + } => true; + "11" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + ..Default::default() + } => true; + "12" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + conditions: serde_json::from_str(r#"{"IpAddress": {"aws:SourceIp": "192.168.1.0/24"}}"#).unwrap(), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetBucketLocation".into(), + bucket: "mybucket".into(), + ..Default::default() + } => false; + "13" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + conditions: serde_json::from_str(r#"{"IpAddress": {"aws:SourceIp": "192.168.1.0/24"}}"#).unwrap(), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:PutObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + conditions: { + let mut h = HashMap::new(); + h.insert("x-amz-copy-source".into(), vec!["mybucket/myobject".into()]); + h.insert("SourceIp".into(), vec!["192.168.1.10".into()]); + h + }, + ..Default::default() + } => true; + "14" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + conditions: serde_json::from_str(r#"{"IpAddress": {"aws:SourceIp": "192.168.1.0/24"}}"#).unwrap(), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + ..Default::default() + } => false; + "15" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + conditions: serde_json::from_str(r#"{"IpAddress": {"aws:SourceIp": "192.168.1.0/24"}}"#).unwrap(), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetBucketLocation".into(), + bucket: "mybucket".into(), + ..Default::default() + } => false; + "16" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + conditions: serde_json::from_str(r#"{"IpAddress": {"aws:SourceIp": "192.168.1.0/24"}}"#).unwrap(), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:PutObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + conditions: { + let mut h = HashMap::new(); + h.insert("x-amz-copy-source".into(), vec!["mybucket/myobject".into()]); + h.insert("SourceIp".into(), vec!["192.168.1.10".into()]); + h + }, + ..Default::default() + } => true; + "17" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Allow, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + conditions: serde_json::from_str(r#"{"IpAddress": {"aws:SourceIp": "192.168.1.0/24"}}"#).unwrap(), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + ..Default::default() + } => false; + "18" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Deny, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + conditions: serde_json::from_str(r#"{"IpAddress": {"aws:SourceIp": "192.168.1.0/24"}}"#).unwrap(), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetBucketLocation".into(), + bucket: "mybucket".into(), + ..Default::default() + } => false; + "19" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Deny, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + conditions: serde_json::from_str(r#"{"IpAddress": {"aws:SourceIp": "192.168.1.0/24"}}"#).unwrap(), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:PutObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + conditions: { + let mut h = HashMap::new(); + h.insert("x-amz-copy-source".into(), vec!["mybucket/myobject".into()]); + h.insert("SourceIp".into(), vec!["192.168.1.10".into()]); + h + }, + ..Default::default() + } => false; + "20" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Deny, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + conditions: serde_json::from_str(r#"{"IpAddress": {"aws:SourceIp": "192.168.1.0/24"}}"#).unwrap(), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + ..Default::default() + } => false; + "21" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Deny, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + conditions: serde_json::from_str(r#"{"IpAddress": {"aws:SourceIp": "192.168.1.0/24"}}"#).unwrap(), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetBucketLocation".into(), + bucket: "mybucket".into(), + ..Default::default() + } => false; + "22" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Deny, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + conditions: serde_json::from_str(r#"{"IpAddress": {"aws:SourceIp": "192.168.1.0/24"}}"#).unwrap(), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:PutObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + conditions: { + let mut h = HashMap::new(); + h.insert("x-amz-copy-source".into(), vec!["mybucket/myobject".into()]); + h.insert("SourceIp".into(), vec!["192.168.1.10".into()]); + h + }, + ..Default::default() + } => false; + "23" +)] +#[test_case( + Policy{ + version: DEFAULT_VERSION.into(), + statements: vec![ + Statement{ + effect: Deny, + actions: ActionSet(vec![Action::S3Action(GetObjectAction), Action::S3Action(PutObjectAction)].into_iter().collect()), + resources: ResourceSet(vec!["arn:aws:s3:::mybucket/myobject*".try_into().unwrap()].into_iter().collect()), + conditions: serde_json::from_str(r#"{"IpAddress": {"aws:SourceIp": "192.168.1.0/24"}}"#).unwrap(), + ..Default::default() + } + ], + ..Default::default() + }, + ArgsBuilder{ + account: "Q3AM3UQ867SPQQA43P2F".into(), + action: "s3:GetObject".into(), + bucket: "mybucket".into(), + object: "myobject".into(), + ..Default::default() + } => false; + "24" +)] +fn policy_is_allowed(policy: Policy, args: ArgsBuilder) -> bool { + policy.is_allowed(&Args { + account: &args.account, + groups: &args.groups, + action: args.action.as_str().try_into().unwrap(), + bucket: &args.bucket, + conditions: &args.conditions, + is_owner: args.is_owner, + object: &args.object, + claims: &args.claims, + deny_only: args.deny_only, + }) +}