Skip to content

Commit a68de58

Browse files
authored
Cleanup IAM definitions (#856)
* Cleanup IAM definitions We have two different definitions of IAM policies and statement. These changes centralize them both into one. These changes also add the `Condition` field that was missing from both implementations. These changes also make `Effect` to be an enum with the default `Allow`, instead of an optional string. This is more ergonomic than checking whether the effect is none or `allow`. Signed-off-by: David Calavera <[email protected]> * Remove unnecesary module. Keep all deserialization logic together, it's easier to manage. Signed-off-by: David Calavera <[email protected]> --------- Signed-off-by: David Calavera <[email protected]>
1 parent bb5a25c commit a68de58

File tree

6 files changed

+234
-72
lines changed

6 files changed

+234
-72
lines changed

lambda-events/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ default = [
8585

8686
activemq = []
8787
alb = ["bytes", "http", "http-body", "http-serde", "query_map"]
88-
apigw = ["bytes", "http", "http-body", "http-serde", "query_map"]
88+
apigw = ["bytes", "http", "http-body", "http-serde", "iam", "query_map"]
8989
appsync = []
9090
autoscaling = ["chrono"]
9191
bedrock_agent_runtime = []

lambda-events/src/custom_serde/mod.rs

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -92,27 +92,6 @@ where
9292
Ok(opt.unwrap_or_default())
9393
}
9494

95-
/// Deserializes `Vec<String>`, from a JSON `string` or `[string]`.
96-
#[cfg(any(feature = "apigw", test))]
97-
pub(crate) fn deserialize_string_or_slice<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
98-
where
99-
D: Deserializer<'de>,
100-
{
101-
#[derive(serde::Deserialize)]
102-
#[serde(untagged)]
103-
enum StringOrSlice {
104-
String(String),
105-
Slice(Vec<String>),
106-
}
107-
108-
let string_or_slice = StringOrSlice::deserialize(deserializer)?;
109-
110-
match string_or_slice {
111-
StringOrSlice::Slice(slice) => Ok(slice),
112-
StringOrSlice::String(s) => Ok(vec![s]),
113-
}
114-
}
115-
11695
#[cfg(test)]
11796
#[allow(deprecated)]
11897
mod test {

lambda-events/src/event/apigw/mod.rs

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use crate::custom_serde::{
2-
deserialize_headers, deserialize_lambda_map, deserialize_nullish_boolean, deserialize_string_or_slice, http_method,
3-
serialize_headers, serialize_multi_value_headers,
2+
deserialize_headers, deserialize_lambda_map, deserialize_nullish_boolean, http_method, serialize_headers,
3+
serialize_multi_value_headers,
44
};
55
use crate::encodings::Body;
6+
use crate::iam::IamPolicyStatement;
67
use http::{HeaderMap, Method};
78
use query_map::QueryMap;
89
use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
@@ -723,30 +724,13 @@ where
723724

724725
/// `ApiGatewayCustomAuthorizerPolicy` represents an IAM policy
725726
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
726-
#[serde(rename_all = "camelCase")]
727+
#[serde(rename_all = "PascalCase")]
727728
pub struct ApiGatewayCustomAuthorizerPolicy {
728729
#[serde(default)]
729-
#[serde(rename = "Version")]
730730
pub version: Option<String>,
731-
#[serde(rename = "Statement")]
732731
pub statement: Vec<IamPolicyStatement>,
733732
}
734733

735-
/// `IamPolicyStatement` represents one statement from IAM policy with action, effect and resource
736-
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
737-
#[serde(rename_all = "camelCase")]
738-
pub struct IamPolicyStatement {
739-
#[serde(rename = "Action")]
740-
#[serde(deserialize_with = "deserialize_string_or_slice")]
741-
pub action: Vec<String>,
742-
#[serde(default)]
743-
#[serde(rename = "Effect")]
744-
pub effect: Option<String>,
745-
#[serde(rename = "Resource")]
746-
#[serde(deserialize_with = "deserialize_string_or_slice")]
747-
pub resource: Vec<String>,
748-
}
749-
750734
fn default_http_method() -> Method {
751735
Method::GET
752736
}
@@ -1045,4 +1029,22 @@ mod test {
10451029
assert_eq!(Some(1), fields.get("clientId").unwrap().as_u64());
10461030
assert_eq!(Some("Exata"), fields.get("clientName").unwrap().as_str());
10471031
}
1032+
1033+
#[test]
1034+
#[cfg(feature = "apigw")]
1035+
fn example_apigw_custom_auth_response_with_statement_condition() {
1036+
use crate::iam::IamPolicyEffect;
1037+
1038+
let data = include_bytes!("../../fixtures/example-apigw-custom-auth-response-with-condition.json");
1039+
let parsed: ApiGatewayCustomAuthorizerResponse = serde_json::from_slice(data).unwrap();
1040+
let output: String = serde_json::to_string(&parsed).unwrap();
1041+
let reparsed: ApiGatewayCustomAuthorizerResponse = serde_json::from_slice(output.as_bytes()).unwrap();
1042+
assert_eq!(parsed, reparsed);
1043+
1044+
let statement = parsed.policy_document.statement.first().unwrap();
1045+
assert_eq!(IamPolicyEffect::Deny, statement.effect);
1046+
1047+
let condition = statement.condition.as_ref().unwrap();
1048+
assert_eq!(vec!["xxx"], condition["StringEquals"]["aws:SourceIp"]);
1049+
}
10481050
}

lambda-events/src/event/iam/mod.rs

Lines changed: 159 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,172 @@
1-
use serde::{Deserialize, Serialize};
1+
use std::{borrow::Cow, collections::HashMap, fmt};
2+
3+
use serde::{
4+
de::{Error as DeError, MapAccess, Visitor},
5+
Deserialize, Deserializer, Serialize,
6+
};
27

38
/// `IamPolicyDocument` represents an IAM policy document.
49
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
5-
#[serde(rename_all = "camelCase")]
10+
#[serde(rename_all = "PascalCase")]
611
pub struct IamPolicyDocument {
712
#[serde(default)]
8-
#[serde(rename = "Version")]
913
pub version: Option<String>,
10-
#[serde(rename = "Statement")]
1114
pub statement: Vec<IamPolicyStatement>,
1215
}
1316

14-
/// `IamPolicyStatement` represents one statement from IAM policy with action, effect and resource.
15-
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
16-
#[serde(rename_all = "camelCase")]
17+
/// `IamPolicyStatement` represents one statement from IAM policy with action, effect and resource
18+
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
19+
#[serde(rename_all = "PascalCase")]
1720
pub struct IamPolicyStatement {
18-
#[serde(rename = "Action")]
21+
#[serde(deserialize_with = "deserialize_string_or_slice")]
1922
pub action: Vec<String>,
20-
#[serde(default)]
21-
#[serde(rename = "Effect")]
22-
pub effect: Option<String>,
23-
#[serde(rename = "Resource")]
23+
#[serde(default = "default_statement_effect")]
24+
pub effect: IamPolicyEffect,
25+
#[serde(deserialize_with = "deserialize_string_or_slice")]
2426
pub resource: Vec<String>,
27+
#[serde(default, deserialize_with = "deserialize_policy_condition")]
28+
pub condition: Option<IamPolicyCondition>,
29+
}
30+
31+
pub type IamPolicyCondition = HashMap<String, HashMap<String, Vec<String>>>;
32+
33+
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
34+
pub enum IamPolicyEffect {
35+
#[default]
36+
Allow,
37+
Deny,
38+
}
39+
40+
fn default_statement_effect() -> IamPolicyEffect {
41+
IamPolicyEffect::Allow
42+
}
43+
44+
#[derive(serde::Deserialize)]
45+
#[serde(untagged)]
46+
enum StringOrSlice {
47+
String(String),
48+
Slice(Vec<String>),
49+
}
50+
51+
/// Deserializes `Vec<String>`, from a JSON `string` or `[string]`.
52+
fn deserialize_string_or_slice<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
53+
where
54+
D: Deserializer<'de>,
55+
{
56+
let string_or_slice = StringOrSlice::deserialize(deserializer)?;
57+
58+
match string_or_slice {
59+
StringOrSlice::Slice(slice) => Ok(slice),
60+
StringOrSlice::String(s) => Ok(vec![s]),
61+
}
62+
}
63+
64+
fn deserialize_policy_condition<'de, D>(de: D) -> Result<Option<IamPolicyCondition>, D::Error>
65+
where
66+
D: Deserializer<'de>,
67+
{
68+
de.deserialize_option(IamPolicyConditionVisitor)
69+
}
70+
71+
struct IamPolicyConditionVisitor;
72+
73+
impl<'de> Visitor<'de> for IamPolicyConditionVisitor {
74+
type Value = Option<IamPolicyCondition>;
75+
76+
// Format a message stating what data this Visitor expects to receive.
77+
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78+
formatter.write_str("lots of things can go wrong with a IAM Policy Condition")
79+
}
80+
81+
fn visit_unit<E>(self) -> Result<Self::Value, E>
82+
where
83+
E: DeError,
84+
{
85+
Ok(None)
86+
}
87+
88+
fn visit_none<E>(self) -> Result<Self::Value, E>
89+
where
90+
E: DeError,
91+
{
92+
Ok(None)
93+
}
94+
95+
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
96+
where
97+
D: Deserializer<'de>,
98+
{
99+
deserializer.deserialize_map(self)
100+
}
101+
102+
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
103+
where
104+
M: MapAccess<'de>,
105+
{
106+
let mut map = HashMap::with_capacity(access.size_hint().unwrap_or(0));
107+
108+
while let Some((key, val)) = access.next_entry::<Cow<'_, str>, HashMap<Cow<'_, str>, StringOrSlice>>()? {
109+
let mut value = HashMap::with_capacity(val.len());
110+
for (val_key, string_or_slice) in val {
111+
let val = match string_or_slice {
112+
StringOrSlice::Slice(slice) => slice,
113+
StringOrSlice::String(s) => vec![s],
114+
};
115+
value.insert(val_key.into_owned(), val);
116+
}
117+
118+
map.insert(key.into_owned(), value);
119+
}
120+
121+
Ok(Some(map))
122+
}
123+
}
124+
125+
#[cfg(test)]
126+
mod tests {
127+
use super::*;
128+
129+
#[test]
130+
fn test_deserialize_string_condition() {
131+
let data = serde_json::json!({
132+
"condition": {
133+
"StringEquals": {
134+
"iam:RegisterSecurityKey": "Activate",
135+
"iam:FIDO-certification": "L1plus"
136+
}
137+
}
138+
});
139+
140+
#[derive(Deserialize)]
141+
struct Test {
142+
#[serde(deserialize_with = "deserialize_policy_condition")]
143+
condition: Option<IamPolicyCondition>,
144+
}
145+
146+
let test: Test = serde_json::from_value(data).unwrap();
147+
let condition = test.condition.unwrap();
148+
assert_eq!(1, condition.len());
149+
150+
assert_eq!(vec!["Activate"], condition["StringEquals"]["iam:RegisterSecurityKey"]);
151+
assert_eq!(vec!["L1plus"], condition["StringEquals"]["iam:FIDO-certification"]);
152+
}
153+
154+
#[test]
155+
fn test_deserialize_slide_condition() {
156+
let data = serde_json::json!({
157+
"condition": {"StringLike": {"s3:prefix": ["janedoe/*"]}}
158+
});
159+
160+
#[derive(Deserialize)]
161+
struct Test {
162+
#[serde(deserialize_with = "deserialize_policy_condition")]
163+
condition: Option<IamPolicyCondition>,
164+
}
165+
166+
let test: Test = serde_json::from_value(data).unwrap();
167+
let condition = test.condition.unwrap();
168+
assert_eq!(1, condition.len());
169+
170+
assert_eq!(vec!["janedoe/*"], condition["StringLike"]["s3:prefix"]);
171+
}
25172
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"principalId": "yyyyyyyy",
3+
"policyDocument": {
4+
"Version": "2012-10-17",
5+
"Statement": [
6+
{
7+
"Action": [
8+
"execute-api:Invoke"
9+
],
10+
"Effect": "Deny",
11+
"Resource": [
12+
"arn:aws:execute-api:{regionId}:{accountId}:{appId}/{stage}/{httpVerb}/[{resource}/[child-resources]]"
13+
],
14+
"Condition": {
15+
"StringEquals": {
16+
"aws:SourceIp": [
17+
"xxx"
18+
]
19+
}
20+
}
21+
}
22+
]
23+
},
24+
"context": {
25+
"stringKey": "value",
26+
"numberKey": "1",
27+
"booleanKey": "true"
28+
},
29+
"usageIdentifierKey": "{api-key}"
30+
}
Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
{
2-
"principalId": "yyyyyyyy",
3-
"policyDocument": {
4-
"Version": "2012-10-17",
5-
"Statement": [
6-
{
7-
"Action": ["execute-api:Invoke"],
8-
"Effect": "Allow|Deny",
9-
"Resource": ["arn:aws:execute-api:{regionId}:{accountId}:{appId}/{stage}/{httpVerb}/[{resource}/[child-resources]]"]
10-
}
11-
]
12-
},
13-
"context": {
14-
"stringKey": "value",
15-
"numberKey": "1",
16-
"booleanKey": "true"
17-
},
18-
"usageIdentifierKey": "{api-key}"
19-
}
2+
"principalId": "yyyyyyyy",
3+
"policyDocument": {
4+
"Version": "2012-10-17",
5+
"Statement": [
6+
{
7+
"Action": [
8+
"execute-api:Invoke"
9+
],
10+
"Effect": "Deny",
11+
"Resource": [
12+
"arn:aws:execute-api:{regionId}:{accountId}:{appId}/{stage}/{httpVerb}/[{resource}/[child-resources]]"
13+
]
14+
}
15+
]
16+
},
17+
"context": {
18+
"stringKey": "value",
19+
"numberKey": "1",
20+
"booleanKey": "true"
21+
},
22+
"usageIdentifierKey": "{api-key}"
23+
}

0 commit comments

Comments
 (0)