Loading crates/s3s-policy/Cargo.toml +3 −0 Original line number Diff line number Diff line Loading @@ -10,3 +10,6 @@ repository.workspace = true license.workspace = true [dependencies] indexmap = { version = "2.4.0", features = ["serde"] } serde = { version = "1.0.208", features = ["derive"] } serde_json = "1.0.125" crates/s3s-policy/src/lib.rs +2 −0 Original line number Diff line number Diff line Loading @@ -8,3 +8,5 @@ #![warn( clippy::dbg_macro, // )] pub mod model; crates/s3s-policy/src/model.rs 0 → 100644 +626 −0 Original line number Diff line number Diff line //! <https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html> use std::marker::PhantomData; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct Policy { pub version: Option<Version>, pub id: Option<Id>, pub statement: OneOrMore<Statement>, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Version { #[serde(rename = "2012-10-17")] V2012_10_17, #[serde(rename = "2008-10-17")] V2008_10_17, } pub type Id = String; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct Statement { pub sid: Option<Sid>, #[serde(flatten)] pub principal: Option<PrincipalRule>, pub effect: Effect, #[serde(flatten)] pub action: ActionRule, #[serde(flatten)] pub resource: ResourceRule, pub condition: Option<ConditionRule>, } pub type Sid = String; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum PrincipalRule { Principal(Principal), NotPrincipal(Principal), } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Principal { Wildcard, Map(IndexMap<String, OneOrMore<String>>), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Effect { Allow, Deny, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ActionRule { Action(WildcardOneOrMore<String>), NotAction(WildcardOneOrMore<String>), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ResourceRule { Resource(WildcardOneOrMore<String>), NotResource(WildcardOneOrMore<String>), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ConditionRule(pub IndexMap<ConditionOperator, ConditionKeyValues>); pub type ConditionOperator = String; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ConditionKeyValues(pub IndexMap<String, OneOrMore<String>>); #[derive(Debug, Clone, PartialEq, Eq)] pub enum OneOrMore<T> { One(T), More(Vec<T>), } #[derive(Debug, Clone, PartialEq, Eq)] pub enum WildcardOneOrMore<T> { Wildcard, One(T), More(Vec<T>), } impl Serialize for Principal { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { match self { Principal::Wildcard => serializer.serialize_str("*"), Principal::Map(map) => map.serialize(serializer), } } } impl<'de> Deserialize<'de> for Principal { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { struct Visitor; impl<'de> serde::de::Visitor<'de> for Visitor { type Value = Principal; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a wildcard or a map") } fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> where E: serde::de::Error, { if value == "*" { return Ok(Principal::Wildcard); } Err(serde::de::Error::invalid_value(serde::de::Unexpected::Str(value), &self)) } fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error> where A: serde::de::MapAccess<'de>, { let de = serde::de::value::MapAccessDeserializer::new(map); IndexMap::deserialize(de).map(Principal::Map) } } deserializer.deserialize_any(Visitor) } } impl<T> Serialize for OneOrMore<T> where T: Serialize, { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { match self { OneOrMore::One(t) => t.serialize(serializer), OneOrMore::More(ts) => ts.serialize(serializer), } } } impl<'de, T> Deserialize<'de> for OneOrMore<T> where T: Deserialize<'de>, { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { struct Visitor<T>(PhantomData<fn() -> T>); impl<'de, T> serde::de::Visitor<'de> for Visitor<T> where T: Deserialize<'de>, { type Value = OneOrMore<T>; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a single value or an array of values") } fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> where E: serde::de::Error, { let de = serde::de::value::StrDeserializer::new(value); T::deserialize(de).map(OneOrMore::One) } fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error> where A: serde::de::MapAccess<'de>, { let de = serde::de::value::MapAccessDeserializer::new(map); T::deserialize(de).map(OneOrMore::One) } fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error> where A: serde::de::SeqAccess<'de>, { let de = serde::de::value::SeqAccessDeserializer::new(seq); <Vec<T>>::deserialize(de).map(OneOrMore::More) } } deserializer.deserialize_any(Visitor(PhantomData)) } } impl<T> From<OneOrMore<T>> for WildcardOneOrMore<T> { fn from(value: OneOrMore<T>) -> Self { match value { OneOrMore::One(t) => WildcardOneOrMore::One(t), OneOrMore::More(ts) => WildcardOneOrMore::More(ts), } } } impl<T> Serialize for WildcardOneOrMore<T> where T: Serialize, { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { match self { WildcardOneOrMore::Wildcard => serializer.serialize_str("*"), WildcardOneOrMore::One(t) => t.serialize(serializer), WildcardOneOrMore::More(ts) => ts.serialize(serializer), } } } impl<'de, T> Deserialize<'de> for WildcardOneOrMore<T> where T: Deserialize<'de>, { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { struct Visitor<T>(PhantomData<fn() -> T>); impl<'de, T> serde::de::Visitor<'de> for Visitor<T> where T: Deserialize<'de>, { type Value = WildcardOneOrMore<T>; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a wildcard, a single value, or an array of values") } fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> where E: serde::de::Error, { if value == "*" { return Ok(WildcardOneOrMore::Wildcard); } let de = serde::de::value::StrDeserializer::new(value); T::deserialize(de).map(WildcardOneOrMore::One) } fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error> where A: serde::de::SeqAccess<'de>, { let de = serde::de::value::SeqAccessDeserializer::new(seq); <Vec<T>>::deserialize(de).map(WildcardOneOrMore::More) } } deserializer.deserialize_any(Visitor(PhantomData)) } } #[cfg(test)] mod tests { use super::*; use super::ActionRule::*; use super::ResourceRule::*; use indexmap::indexmap; fn one(s: &str) -> WildcardOneOrMore<String> { WildcardOneOrMore::One(s.to_owned()) } fn more(ss: &[&str]) -> WildcardOneOrMore<String> { WildcardOneOrMore::More(ss.iter().copied().map(ToOwned::to_owned).collect()) } #[test] fn version_serde() { let cases = [ (Version::V2012_10_17, "\"2012-10-17\""), (Version::V2008_10_17, "\"2008-10-17\""), ]; for (enum_, str_) in cases { let ser = serde_json::to_string(&enum_).unwrap(); assert_eq!(ser, str_); let de: Version = serde_json::from_str(str_).unwrap(); assert_eq!(de, enum_); } } #[test] fn effect_serde() { let cases = [ (Effect::Allow, "\"Allow\""), // (Effect::Deny, "\"Deny\""), ]; for (enum_, str_) in cases { let ser = serde_json::to_string(&enum_).unwrap(); assert_eq!(ser, str_); let de: Effect = serde_json::from_str(str_).unwrap(); assert_eq!(de, enum_); } } #[test] fn action_serde() { let cases = [ ( Action(WildcardOneOrMore::Wildcard), // r#"{"Action":"*"}"#, ), ( Action(one("s3:GetObject")), // r#"{"Action":"s3:GetObject"}"#, ), ( Action(more(&["s3:GetObject", "s3:PutObject"])), r#"{"Action":["s3:GetObject","s3:PutObject"]}"#, ), ( NotAction(WildcardOneOrMore::Wildcard), // r#"{"NotAction":"*"}"#, ), ( NotAction(one("s3:GetObject")), // r#"{"NotAction":"s3:GetObject"}"#, ), ( NotAction(more(&["s3:GetObject", "s3:PutObject"])), r#"{"NotAction":["s3:GetObject","s3:PutObject"]}"#, ), ]; for (enum_, str_) in cases { let ser = serde_json::to_string(&enum_).unwrap(); assert_eq!(ser, str_); let de: ActionRule = serde_json::from_str(str_).unwrap(); assert_eq!(de, enum_); } } #[test] fn resource_serde() { let cases = [ ( Resource(WildcardOneOrMore::Wildcard), // r#"{"Resource":"*"}"#, ), ( Resource(one("arn:aws:s3:::examplebucket/*")), // r#"{"Resource":"arn:aws:s3:::examplebucket/*"}"#, ), ( Resource(more(&["arn:aws:s3:::examplebucket/*", "arn:aws:s3:::examplebucket"])), r#"{"Resource":["arn:aws:s3:::examplebucket/*","arn:aws:s3:::examplebucket"]}"#, ), ( NotResource(WildcardOneOrMore::Wildcard), // r#"{"NotResource":"*"}"#, ), ( NotResource(one("arn:aws:s3:::examplebucket/*")), // r#"{"NotResource":"arn:aws:s3:::examplebucket/*"}"#, ), ( NotResource(more(&["arn:aws:s3:::examplebucket/*", "arn:aws:s3:::examplebucket"])), r#"{"NotResource":["arn:aws:s3:::examplebucket/*","arn:aws:s3:::examplebucket"]}"#, ), ]; for (enum_, str_) in cases { let ser = serde_json::to_string(&enum_).unwrap(); assert_eq!(ser, str_); let de: ResourceRule = serde_json::from_str(str_).unwrap(); assert_eq!(de, enum_); } } #[test] fn principal_serde() { let cases = [ ( PrincipalRule::Principal(Principal::Wildcard), // r#"{"Principal":"*"}"#, ), ( PrincipalRule::Principal(Principal::Map(indexmap! { "AWS".to_owned() => OneOrMore::One("arn:aws:iam::123456789012:root".to_owned()) })), r#"{"Principal":{"AWS":"arn:aws:iam::123456789012:root"}}"#, ), ( PrincipalRule::Principal(Principal::Map(indexmap! { "Service".to_owned() => OneOrMore::More(vec![ "ecs.amazonaws.com".to_owned(), "elasticloadbalancing.amazonaws.com".to_owned(), ]) })), r#"{"Principal":{"Service":["ecs.amazonaws.com","elasticloadbalancing.amazonaws.com"]}}"#, ), ( PrincipalRule::NotPrincipal(Principal::Wildcard), // r#"{"NotPrincipal":"*"}"#, ), ( PrincipalRule::NotPrincipal(Principal::Map(indexmap! { "AWS".to_owned() => OneOrMore::One("arn:aws:iam::123456789012:root".to_owned()) })), r#"{"NotPrincipal":{"AWS":"arn:aws:iam::123456789012:root"}}"#, ), ( PrincipalRule::NotPrincipal(Principal::Map(indexmap! { "Service".to_owned() => OneOrMore::More(vec![ "ecs.amazonaws.com".to_owned(), "elasticloadbalancing.amazonaws.com".to_owned(), ]) })), r#"{"NotPrincipal":{"Service":["ecs.amazonaws.com","elasticloadbalancing.amazonaws.com"]}}"#, ), ]; for (enum_, str_) in cases { let ser = serde_json::to_string(&enum_).unwrap(); assert_eq!(ser, str_); let de: PrincipalRule = serde_json::from_str(str_).unwrap(); assert_eq!(de, enum_); } } /// <https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#access_policies-json> #[test] fn example1() { let json = r#" { "Version": "2012-10-17", "Statement": [ { "Sid": "FirstStatement", "Effect": "Allow", "Action": ["iam:ChangePassword"], "Resource": "*" }, { "Sid": "SecondStatement", "Effect": "Allow", "Action": "s3:ListAllMyBuckets", "Resource": "*" }, { "Sid": "ThirdStatement", "Effect": "Allow", "Action": [ "s3:List*", "s3:Get*" ], "Resource": [ "arn:aws:s3:::confidential-data", "arn:aws:s3:::confidential-data/*" ], "Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}} } ] } "#; let policy: Policy = serde_json::from_str(json).unwrap(); let expected = Policy { version: Some(Version::V2012_10_17), id: None, statement: OneOrMore::More(vec![ Statement { sid: Some("FirstStatement".to_owned()), effect: Effect::Allow, action: Action(more(&["iam:ChangePassword"])), resource: Resource(WildcardOneOrMore::Wildcard), principal: None, condition: None, }, Statement { sid: Some("SecondStatement".to_owned()), effect: Effect::Allow, action: Action(one("s3:ListAllMyBuckets")), resource: Resource(WildcardOneOrMore::Wildcard), principal: None, condition: None, }, Statement { sid: Some("ThirdStatement".to_owned()), effect: Effect::Allow, action: Action(more(&["s3:List*", "s3:Get*"])), resource: Resource(more(&["arn:aws:s3:::confidential-data", "arn:aws:s3:::confidential-data/*"])), principal: None, condition: Some(ConditionRule(indexmap! { "Bool".to_owned() => ConditionKeyValues( indexmap! { "aws:MultiFactorAuthPresent".to_owned() => OneOrMore::One("true".to_owned()) } ) })), }, ]), }; assert_eq!(policy, expected); { let ser = serde_json::to_string(&policy).unwrap(); let de: Policy = serde_json::from_str(&ser).unwrap(); assert_eq!(de, policy); } } /// <https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#access_policies-json> #[test] fn example2() { let json = r#" { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::example_bucket" } } "#; let policy: Policy = serde_json::from_str(json).unwrap(); let expected = Policy { version: Some(Version::V2012_10_17), id: None, statement: OneOrMore::One(Statement { sid: None, effect: Effect::Allow, action: Action(one("s3:ListBucket")), resource: Resource(one("arn:aws:s3:::example_bucket")), principal: None, condition: None, }), }; assert_eq!(policy, expected); { let ser = serde_json::to_string(&policy).unwrap(); let de: Policy = serde_json::from_str(&ser).unwrap(); assert_eq!(de, policy); } } /// <https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#access_policies-json> #[test] fn example3() { let json = r#" { "Version": "2012-10-17", "Statement": [{ "Sid": "1", "Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam::account-id:root"]}, "Action": "s3:*", "Resource": [ "arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/*" ] }] } "#; let policy: Policy = serde_json::from_str(json).unwrap(); let expected = Policy { version: Some(Version::V2012_10_17), id: None, statement: OneOrMore::More(vec![Statement { sid: Some("1".to_owned()), effect: Effect::Allow, action: Action(one("s3:*")), resource: Resource(more(&["arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/*"])), principal: Some(PrincipalRule::Principal(Principal::Map(indexmap! { "AWS".to_owned() => OneOrMore::More(vec!["arn:aws:iam::account-id:root".to_owned()]) }))), condition: None, }]), }; assert_eq!(policy, expected); { let ser = serde_json::to_string(&policy).unwrap(); let de: Policy = serde_json::from_str(&ser).unwrap(); assert_eq!(de, policy); } } } Loading
crates/s3s-policy/Cargo.toml +3 −0 Original line number Diff line number Diff line Loading @@ -10,3 +10,6 @@ repository.workspace = true license.workspace = true [dependencies] indexmap = { version = "2.4.0", features = ["serde"] } serde = { version = "1.0.208", features = ["derive"] } serde_json = "1.0.125"
crates/s3s-policy/src/lib.rs +2 −0 Original line number Diff line number Diff line Loading @@ -8,3 +8,5 @@ #![warn( clippy::dbg_macro, // )] pub mod model;
crates/s3s-policy/src/model.rs 0 → 100644 +626 −0 Original line number Diff line number Diff line //! <https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html> use std::marker::PhantomData; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct Policy { pub version: Option<Version>, pub id: Option<Id>, pub statement: OneOrMore<Statement>, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Version { #[serde(rename = "2012-10-17")] V2012_10_17, #[serde(rename = "2008-10-17")] V2008_10_17, } pub type Id = String; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct Statement { pub sid: Option<Sid>, #[serde(flatten)] pub principal: Option<PrincipalRule>, pub effect: Effect, #[serde(flatten)] pub action: ActionRule, #[serde(flatten)] pub resource: ResourceRule, pub condition: Option<ConditionRule>, } pub type Sid = String; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum PrincipalRule { Principal(Principal), NotPrincipal(Principal), } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Principal { Wildcard, Map(IndexMap<String, OneOrMore<String>>), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Effect { Allow, Deny, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ActionRule { Action(WildcardOneOrMore<String>), NotAction(WildcardOneOrMore<String>), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ResourceRule { Resource(WildcardOneOrMore<String>), NotResource(WildcardOneOrMore<String>), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ConditionRule(pub IndexMap<ConditionOperator, ConditionKeyValues>); pub type ConditionOperator = String; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ConditionKeyValues(pub IndexMap<String, OneOrMore<String>>); #[derive(Debug, Clone, PartialEq, Eq)] pub enum OneOrMore<T> { One(T), More(Vec<T>), } #[derive(Debug, Clone, PartialEq, Eq)] pub enum WildcardOneOrMore<T> { Wildcard, One(T), More(Vec<T>), } impl Serialize for Principal { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { match self { Principal::Wildcard => serializer.serialize_str("*"), Principal::Map(map) => map.serialize(serializer), } } } impl<'de> Deserialize<'de> for Principal { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { struct Visitor; impl<'de> serde::de::Visitor<'de> for Visitor { type Value = Principal; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a wildcard or a map") } fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> where E: serde::de::Error, { if value == "*" { return Ok(Principal::Wildcard); } Err(serde::de::Error::invalid_value(serde::de::Unexpected::Str(value), &self)) } fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error> where A: serde::de::MapAccess<'de>, { let de = serde::de::value::MapAccessDeserializer::new(map); IndexMap::deserialize(de).map(Principal::Map) } } deserializer.deserialize_any(Visitor) } } impl<T> Serialize for OneOrMore<T> where T: Serialize, { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { match self { OneOrMore::One(t) => t.serialize(serializer), OneOrMore::More(ts) => ts.serialize(serializer), } } } impl<'de, T> Deserialize<'de> for OneOrMore<T> where T: Deserialize<'de>, { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { struct Visitor<T>(PhantomData<fn() -> T>); impl<'de, T> serde::de::Visitor<'de> for Visitor<T> where T: Deserialize<'de>, { type Value = OneOrMore<T>; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a single value or an array of values") } fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> where E: serde::de::Error, { let de = serde::de::value::StrDeserializer::new(value); T::deserialize(de).map(OneOrMore::One) } fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error> where A: serde::de::MapAccess<'de>, { let de = serde::de::value::MapAccessDeserializer::new(map); T::deserialize(de).map(OneOrMore::One) } fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error> where A: serde::de::SeqAccess<'de>, { let de = serde::de::value::SeqAccessDeserializer::new(seq); <Vec<T>>::deserialize(de).map(OneOrMore::More) } } deserializer.deserialize_any(Visitor(PhantomData)) } } impl<T> From<OneOrMore<T>> for WildcardOneOrMore<T> { fn from(value: OneOrMore<T>) -> Self { match value { OneOrMore::One(t) => WildcardOneOrMore::One(t), OneOrMore::More(ts) => WildcardOneOrMore::More(ts), } } } impl<T> Serialize for WildcardOneOrMore<T> where T: Serialize, { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { match self { WildcardOneOrMore::Wildcard => serializer.serialize_str("*"), WildcardOneOrMore::One(t) => t.serialize(serializer), WildcardOneOrMore::More(ts) => ts.serialize(serializer), } } } impl<'de, T> Deserialize<'de> for WildcardOneOrMore<T> where T: Deserialize<'de>, { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { struct Visitor<T>(PhantomData<fn() -> T>); impl<'de, T> serde::de::Visitor<'de> for Visitor<T> where T: Deserialize<'de>, { type Value = WildcardOneOrMore<T>; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a wildcard, a single value, or an array of values") } fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> where E: serde::de::Error, { if value == "*" { return Ok(WildcardOneOrMore::Wildcard); } let de = serde::de::value::StrDeserializer::new(value); T::deserialize(de).map(WildcardOneOrMore::One) } fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error> where A: serde::de::SeqAccess<'de>, { let de = serde::de::value::SeqAccessDeserializer::new(seq); <Vec<T>>::deserialize(de).map(WildcardOneOrMore::More) } } deserializer.deserialize_any(Visitor(PhantomData)) } } #[cfg(test)] mod tests { use super::*; use super::ActionRule::*; use super::ResourceRule::*; use indexmap::indexmap; fn one(s: &str) -> WildcardOneOrMore<String> { WildcardOneOrMore::One(s.to_owned()) } fn more(ss: &[&str]) -> WildcardOneOrMore<String> { WildcardOneOrMore::More(ss.iter().copied().map(ToOwned::to_owned).collect()) } #[test] fn version_serde() { let cases = [ (Version::V2012_10_17, "\"2012-10-17\""), (Version::V2008_10_17, "\"2008-10-17\""), ]; for (enum_, str_) in cases { let ser = serde_json::to_string(&enum_).unwrap(); assert_eq!(ser, str_); let de: Version = serde_json::from_str(str_).unwrap(); assert_eq!(de, enum_); } } #[test] fn effect_serde() { let cases = [ (Effect::Allow, "\"Allow\""), // (Effect::Deny, "\"Deny\""), ]; for (enum_, str_) in cases { let ser = serde_json::to_string(&enum_).unwrap(); assert_eq!(ser, str_); let de: Effect = serde_json::from_str(str_).unwrap(); assert_eq!(de, enum_); } } #[test] fn action_serde() { let cases = [ ( Action(WildcardOneOrMore::Wildcard), // r#"{"Action":"*"}"#, ), ( Action(one("s3:GetObject")), // r#"{"Action":"s3:GetObject"}"#, ), ( Action(more(&["s3:GetObject", "s3:PutObject"])), r#"{"Action":["s3:GetObject","s3:PutObject"]}"#, ), ( NotAction(WildcardOneOrMore::Wildcard), // r#"{"NotAction":"*"}"#, ), ( NotAction(one("s3:GetObject")), // r#"{"NotAction":"s3:GetObject"}"#, ), ( NotAction(more(&["s3:GetObject", "s3:PutObject"])), r#"{"NotAction":["s3:GetObject","s3:PutObject"]}"#, ), ]; for (enum_, str_) in cases { let ser = serde_json::to_string(&enum_).unwrap(); assert_eq!(ser, str_); let de: ActionRule = serde_json::from_str(str_).unwrap(); assert_eq!(de, enum_); } } #[test] fn resource_serde() { let cases = [ ( Resource(WildcardOneOrMore::Wildcard), // r#"{"Resource":"*"}"#, ), ( Resource(one("arn:aws:s3:::examplebucket/*")), // r#"{"Resource":"arn:aws:s3:::examplebucket/*"}"#, ), ( Resource(more(&["arn:aws:s3:::examplebucket/*", "arn:aws:s3:::examplebucket"])), r#"{"Resource":["arn:aws:s3:::examplebucket/*","arn:aws:s3:::examplebucket"]}"#, ), ( NotResource(WildcardOneOrMore::Wildcard), // r#"{"NotResource":"*"}"#, ), ( NotResource(one("arn:aws:s3:::examplebucket/*")), // r#"{"NotResource":"arn:aws:s3:::examplebucket/*"}"#, ), ( NotResource(more(&["arn:aws:s3:::examplebucket/*", "arn:aws:s3:::examplebucket"])), r#"{"NotResource":["arn:aws:s3:::examplebucket/*","arn:aws:s3:::examplebucket"]}"#, ), ]; for (enum_, str_) in cases { let ser = serde_json::to_string(&enum_).unwrap(); assert_eq!(ser, str_); let de: ResourceRule = serde_json::from_str(str_).unwrap(); assert_eq!(de, enum_); } } #[test] fn principal_serde() { let cases = [ ( PrincipalRule::Principal(Principal::Wildcard), // r#"{"Principal":"*"}"#, ), ( PrincipalRule::Principal(Principal::Map(indexmap! { "AWS".to_owned() => OneOrMore::One("arn:aws:iam::123456789012:root".to_owned()) })), r#"{"Principal":{"AWS":"arn:aws:iam::123456789012:root"}}"#, ), ( PrincipalRule::Principal(Principal::Map(indexmap! { "Service".to_owned() => OneOrMore::More(vec![ "ecs.amazonaws.com".to_owned(), "elasticloadbalancing.amazonaws.com".to_owned(), ]) })), r#"{"Principal":{"Service":["ecs.amazonaws.com","elasticloadbalancing.amazonaws.com"]}}"#, ), ( PrincipalRule::NotPrincipal(Principal::Wildcard), // r#"{"NotPrincipal":"*"}"#, ), ( PrincipalRule::NotPrincipal(Principal::Map(indexmap! { "AWS".to_owned() => OneOrMore::One("arn:aws:iam::123456789012:root".to_owned()) })), r#"{"NotPrincipal":{"AWS":"arn:aws:iam::123456789012:root"}}"#, ), ( PrincipalRule::NotPrincipal(Principal::Map(indexmap! { "Service".to_owned() => OneOrMore::More(vec![ "ecs.amazonaws.com".to_owned(), "elasticloadbalancing.amazonaws.com".to_owned(), ]) })), r#"{"NotPrincipal":{"Service":["ecs.amazonaws.com","elasticloadbalancing.amazonaws.com"]}}"#, ), ]; for (enum_, str_) in cases { let ser = serde_json::to_string(&enum_).unwrap(); assert_eq!(ser, str_); let de: PrincipalRule = serde_json::from_str(str_).unwrap(); assert_eq!(de, enum_); } } /// <https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#access_policies-json> #[test] fn example1() { let json = r#" { "Version": "2012-10-17", "Statement": [ { "Sid": "FirstStatement", "Effect": "Allow", "Action": ["iam:ChangePassword"], "Resource": "*" }, { "Sid": "SecondStatement", "Effect": "Allow", "Action": "s3:ListAllMyBuckets", "Resource": "*" }, { "Sid": "ThirdStatement", "Effect": "Allow", "Action": [ "s3:List*", "s3:Get*" ], "Resource": [ "arn:aws:s3:::confidential-data", "arn:aws:s3:::confidential-data/*" ], "Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}} } ] } "#; let policy: Policy = serde_json::from_str(json).unwrap(); let expected = Policy { version: Some(Version::V2012_10_17), id: None, statement: OneOrMore::More(vec![ Statement { sid: Some("FirstStatement".to_owned()), effect: Effect::Allow, action: Action(more(&["iam:ChangePassword"])), resource: Resource(WildcardOneOrMore::Wildcard), principal: None, condition: None, }, Statement { sid: Some("SecondStatement".to_owned()), effect: Effect::Allow, action: Action(one("s3:ListAllMyBuckets")), resource: Resource(WildcardOneOrMore::Wildcard), principal: None, condition: None, }, Statement { sid: Some("ThirdStatement".to_owned()), effect: Effect::Allow, action: Action(more(&["s3:List*", "s3:Get*"])), resource: Resource(more(&["arn:aws:s3:::confidential-data", "arn:aws:s3:::confidential-data/*"])), principal: None, condition: Some(ConditionRule(indexmap! { "Bool".to_owned() => ConditionKeyValues( indexmap! { "aws:MultiFactorAuthPresent".to_owned() => OneOrMore::One("true".to_owned()) } ) })), }, ]), }; assert_eq!(policy, expected); { let ser = serde_json::to_string(&policy).unwrap(); let de: Policy = serde_json::from_str(&ser).unwrap(); assert_eq!(de, policy); } } /// <https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#access_policies-json> #[test] fn example2() { let json = r#" { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::example_bucket" } } "#; let policy: Policy = serde_json::from_str(json).unwrap(); let expected = Policy { version: Some(Version::V2012_10_17), id: None, statement: OneOrMore::One(Statement { sid: None, effect: Effect::Allow, action: Action(one("s3:ListBucket")), resource: Resource(one("arn:aws:s3:::example_bucket")), principal: None, condition: None, }), }; assert_eq!(policy, expected); { let ser = serde_json::to_string(&policy).unwrap(); let de: Policy = serde_json::from_str(&ser).unwrap(); assert_eq!(de, policy); } } /// <https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#access_policies-json> #[test] fn example3() { let json = r#" { "Version": "2012-10-17", "Statement": [{ "Sid": "1", "Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam::account-id:root"]}, "Action": "s3:*", "Resource": [ "arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/*" ] }] } "#; let policy: Policy = serde_json::from_str(json).unwrap(); let expected = Policy { version: Some(Version::V2012_10_17), id: None, statement: OneOrMore::More(vec![Statement { sid: Some("1".to_owned()), effect: Effect::Allow, action: Action(one("s3:*")), resource: Resource(more(&["arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/*"])), principal: Some(PrincipalRule::Principal(Principal::Map(indexmap! { "AWS".to_owned() => OneOrMore::More(vec!["arn:aws:iam::account-id:root".to_owned()]) }))), condition: None, }]), }; assert_eq!(policy, expected); { let ser = serde_json::to_string(&policy).unwrap(); let de: Policy = serde_json::from_str(&ser).unwrap(); assert_eq!(de, policy); } } }