Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup IAM definitions #856

Merged
merged 2 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lambda-events/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ default = [

activemq = []
alb = ["bytes", "http", "http-body", "http-serde", "query_map"]
apigw = ["bytes", "http", "http-body", "http-serde", "query_map"]
apigw = ["bytes", "http", "http-body", "http-serde", "iam", "query_map"]
appsync = []
autoscaling = ["chrono"]
bedrock_agent_runtime = []
Expand Down
21 changes: 0 additions & 21 deletions lambda-events/src/custom_serde/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,27 +92,6 @@ where
Ok(opt.unwrap_or_default())
}

/// Deserializes `Vec<String>`, from a JSON `string` or `[string]`.
#[cfg(any(feature = "apigw", test))]
pub(crate) fn deserialize_string_or_slice<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum StringOrSlice {
String(String),
Slice(Vec<String>),
}

let string_or_slice = StringOrSlice::deserialize(deserializer)?;

match string_or_slice {
StringOrSlice::Slice(slice) => Ok(slice),
StringOrSlice::String(s) => Ok(vec![s]),
}
}

#[cfg(test)]
#[allow(deprecated)]
mod test {
Expand Down
42 changes: 22 additions & 20 deletions lambda-events/src/event/apigw/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::custom_serde::{
deserialize_headers, deserialize_lambda_map, deserialize_nullish_boolean, deserialize_string_or_slice, http_method,
serialize_headers, serialize_multi_value_headers,
deserialize_headers, deserialize_lambda_map, deserialize_nullish_boolean, http_method, serialize_headers,
serialize_multi_value_headers,
};
use crate::encodings::Body;
use crate::iam::IamPolicyStatement;
use http::{HeaderMap, Method};
use query_map::QueryMap;
use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
Expand Down Expand Up @@ -723,30 +724,13 @@ where

/// `ApiGatewayCustomAuthorizerPolicy` represents an IAM policy
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "PascalCase")]
pub struct ApiGatewayCustomAuthorizerPolicy {
#[serde(default)]
#[serde(rename = "Version")]
pub version: Option<String>,
#[serde(rename = "Statement")]
pub statement: Vec<IamPolicyStatement>,
}

/// `IamPolicyStatement` represents one statement from IAM policy with action, effect and resource
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IamPolicyStatement {
#[serde(rename = "Action")]
#[serde(deserialize_with = "deserialize_string_or_slice")]
pub action: Vec<String>,
#[serde(default)]
#[serde(rename = "Effect")]
pub effect: Option<String>,
#[serde(rename = "Resource")]
#[serde(deserialize_with = "deserialize_string_or_slice")]
pub resource: Vec<String>,
}

fn default_http_method() -> Method {
Method::GET
}
Expand Down Expand Up @@ -1045,4 +1029,22 @@ mod test {
assert_eq!(Some(1), fields.get("clientId").unwrap().as_u64());
assert_eq!(Some("Exata"), fields.get("clientName").unwrap().as_str());
}

#[test]
#[cfg(feature = "apigw")]
fn example_apigw_custom_auth_response_with_statement_condition() {
use crate::iam::IamPolicyEffect;

let data = include_bytes!("../../fixtures/example-apigw-custom-auth-response-with-condition.json");
let parsed: ApiGatewayCustomAuthorizerResponse = serde_json::from_slice(data).unwrap();
let output: String = serde_json::to_string(&parsed).unwrap();
let reparsed: ApiGatewayCustomAuthorizerResponse = serde_json::from_slice(output.as_bytes()).unwrap();
assert_eq!(parsed, reparsed);

let statement = parsed.policy_document.statement.first().unwrap();
assert_eq!(IamPolicyEffect::Deny, statement.effect);

let condition = statement.condition.as_ref().unwrap();
assert_eq!(vec!["xxx"], condition["StringEquals"]["aws:SourceIp"]);
}
}
171 changes: 159 additions & 12 deletions lambda-events/src/event/iam/mod.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,172 @@
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, collections::HashMap, fmt};

use serde::{
de::{Error as DeError, MapAccess, Visitor},
Deserialize, Deserializer, Serialize,
};

/// `IamPolicyDocument` represents an IAM policy document.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "PascalCase")]
pub struct IamPolicyDocument {
#[serde(default)]
#[serde(rename = "Version")]
pub version: Option<String>,
#[serde(rename = "Statement")]
pub statement: Vec<IamPolicyStatement>,
}

/// `IamPolicyStatement` represents one statement from IAM policy with action, effect and resource.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
/// `IamPolicyStatement` represents one statement from IAM policy with action, effect and resource
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct IamPolicyStatement {
#[serde(rename = "Action")]
#[serde(deserialize_with = "deserialize_string_or_slice")]
pub action: Vec<String>,
#[serde(default)]
#[serde(rename = "Effect")]
pub effect: Option<String>,
#[serde(rename = "Resource")]
#[serde(default = "default_statement_effect")]
pub effect: IamPolicyEffect,
#[serde(deserialize_with = "deserialize_string_or_slice")]
pub resource: Vec<String>,
#[serde(default, deserialize_with = "deserialize_policy_condition")]
pub condition: Option<IamPolicyCondition>,
}

pub type IamPolicyCondition = HashMap<String, HashMap<String, Vec<String>>>;

#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum IamPolicyEffect {
#[default]
Allow,
Deny,
}

fn default_statement_effect() -> IamPolicyEffect {
IamPolicyEffect::Allow
}

#[derive(serde::Deserialize)]
#[serde(untagged)]
enum StringOrSlice {
String(String),
Slice(Vec<String>),
}

/// Deserializes `Vec<String>`, from a JSON `string` or `[string]`.
fn deserialize_string_or_slice<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
let string_or_slice = StringOrSlice::deserialize(deserializer)?;

match string_or_slice {
StringOrSlice::Slice(slice) => Ok(slice),
StringOrSlice::String(s) => Ok(vec![s]),
}
}

fn deserialize_policy_condition<'de, D>(de: D) -> Result<Option<IamPolicyCondition>, D::Error>
where
D: Deserializer<'de>,
{
de.deserialize_option(IamPolicyConditionVisitor)
}

struct IamPolicyConditionVisitor;

impl<'de> Visitor<'de> for IamPolicyConditionVisitor {
type Value = Option<IamPolicyCondition>;

// Format a message stating what data this Visitor expects to receive.
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("lots of things can go wrong with a IAM Policy Condition")
}

fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: DeError,
{
Ok(None)
}

fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: DeError,
{
Ok(None)
}

fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(self)
}

fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut map = HashMap::with_capacity(access.size_hint().unwrap_or(0));

while let Some((key, val)) = access.next_entry::<Cow<'_, str>, HashMap<Cow<'_, str>, StringOrSlice>>()? {
let mut value = HashMap::with_capacity(val.len());
for (val_key, string_or_slice) in val {
let val = match string_or_slice {
StringOrSlice::Slice(slice) => slice,
StringOrSlice::String(s) => vec![s],
};
value.insert(val_key.into_owned(), val);
}

map.insert(key.into_owned(), value);
}

Ok(Some(map))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_deserialize_string_condition() {
let data = serde_json::json!({
"condition": {
"StringEquals": {
"iam:RegisterSecurityKey": "Activate",
"iam:FIDO-certification": "L1plus"
}
}
});

#[derive(Deserialize)]
struct Test {
#[serde(deserialize_with = "deserialize_policy_condition")]
condition: Option<IamPolicyCondition>,
}

let test: Test = serde_json::from_value(data).unwrap();
let condition = test.condition.unwrap();
assert_eq!(1, condition.len());

assert_eq!(vec!["Activate"], condition["StringEquals"]["iam:RegisterSecurityKey"]);
assert_eq!(vec!["L1plus"], condition["StringEquals"]["iam:FIDO-certification"]);
}

#[test]
fn test_deserialize_slide_condition() {
let data = serde_json::json!({
"condition": {"StringLike": {"s3:prefix": ["janedoe/*"]}}
});

#[derive(Deserialize)]
struct Test {
#[serde(deserialize_with = "deserialize_policy_condition")]
condition: Option<IamPolicyCondition>,
}

let test: Test = serde_json::from_value(data).unwrap();
let condition = test.condition.unwrap();
assert_eq!(1, condition.len());

assert_eq!(vec!["janedoe/*"], condition["StringLike"]["s3:prefix"]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"principalId": "yyyyyyyy",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"execute-api:Invoke"
],
"Effect": "Deny",
"Resource": [
"arn:aws:execute-api:{regionId}:{accountId}:{appId}/{stage}/{httpVerb}/[{resource}/[child-resources]]"
],
"Condition": {
"StringEquals": {
"aws:SourceIp": [
"xxx"
]
}
}
}
]
},
"context": {
"stringKey": "value",
"numberKey": "1",
"booleanKey": "true"
},
"usageIdentifierKey": "{api-key}"
}
40 changes: 22 additions & 18 deletions lambda-events/src/fixtures/example-apigw-custom-auth-response.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
{
"principalId": "yyyyyyyy",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": ["execute-api:Invoke"],
"Effect": "Allow|Deny",
"Resource": ["arn:aws:execute-api:{regionId}:{accountId}:{appId}/{stage}/{httpVerb}/[{resource}/[child-resources]]"]
}
]
},
"context": {
"stringKey": "value",
"numberKey": "1",
"booleanKey": "true"
},
"usageIdentifierKey": "{api-key}"
}
"principalId": "yyyyyyyy",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"execute-api:Invoke"
],
"Effect": "Deny",
"Resource": [
"arn:aws:execute-api:{regionId}:{accountId}:{appId}/{stage}/{httpVerb}/[{resource}/[child-resources]]"
]
}
]
},
"context": {
"stringKey": "value",
"numberKey": "1",
"booleanKey": "true"
},
"usageIdentifierKey": "{api-key}"
}
Loading