use super::*;
pub(crate) fn partition_for_region(region: &str) -> &str {
if region.starts_with("cn-") {
"aws-cn"
} else if region.starts_with("us-iso-") {
"aws-iso"
} else if region.starts_with("us-isob-") {
"aws-iso-b"
} else if region.starts_with("us-isof-") {
"aws-iso-f"
} else if region.starts_with("eu-isoe-") {
"aws-iso-e"
} else {
"aws"
}
}
pub(crate) fn is_mutating_action(action: &str) -> bool {
matches!(
action,
"CreateUser"
| "DeleteUser"
| "UpdateUser"
| "TagUser"
| "UntagUser"
| "CreateAccessKey"
| "DeleteAccessKey"
| "UpdateAccessKey"
| "CreateRole"
| "DeleteRole"
| "UpdateRole"
| "UpdateRoleDescription"
| "UpdateAssumeRolePolicy"
| "TagRole"
| "UntagRole"
| "PutRolePermissionsBoundary"
| "DeleteRolePermissionsBoundary"
| "CreatePolicy"
| "DeletePolicy"
| "TagPolicy"
| "UntagPolicy"
| "CreatePolicyVersion"
| "DeletePolicyVersion"
| "SetDefaultPolicyVersion"
| "AttachRolePolicy"
| "DetachRolePolicy"
| "PutRolePolicy"
| "DeleteRolePolicy"
| "AttachUserPolicy"
| "DetachUserPolicy"
| "PutUserPolicy"
| "DeleteUserPolicy"
| "PutUserPermissionsBoundary"
| "DeleteUserPermissionsBoundary"
| "CreateGroup"
| "DeleteGroup"
| "UpdateGroup"
| "AddUserToGroup"
| "RemoveUserFromGroup"
| "PutGroupPolicy"
| "DeleteGroupPolicy"
| "AttachGroupPolicy"
| "DetachGroupPolicy"
| "CreateInstanceProfile"
| "DeleteInstanceProfile"
| "AddRoleToInstanceProfile"
| "RemoveRoleFromInstanceProfile"
| "TagInstanceProfile"
| "UntagInstanceProfile"
| "CreateLoginProfile"
| "UpdateLoginProfile"
| "DeleteLoginProfile"
| "CreateSAMLProvider"
| "DeleteSAMLProvider"
| "UpdateSAMLProvider"
| "CreateOpenIDConnectProvider"
| "DeleteOpenIDConnectProvider"
| "UpdateOpenIDConnectProviderThumbprint"
| "AddClientIDToOpenIDConnectProvider"
| "RemoveClientIDFromOpenIDConnectProvider"
| "TagOpenIDConnectProvider"
| "UntagOpenIDConnectProvider"
| "UploadServerCertificate"
| "DeleteServerCertificate"
| "UploadSigningCertificate"
| "UpdateSigningCertificate"
| "DeleteSigningCertificate"
| "UploadSSHPublicKey"
| "UpdateSSHPublicKey"
| "DeleteSSHPublicKey"
| "CreateServiceLinkedRole"
| "DeleteServiceLinkedRole"
| "CreateAccountAlias"
| "DeleteAccountAlias"
| "UpdateAccountPasswordPolicy"
| "DeleteAccountPasswordPolicy"
| "GenerateCredentialReport"
| "CreateVirtualMFADevice"
| "DeleteVirtualMFADevice"
| "EnableMFADevice"
| "DeactivateMFADevice"
| "UpdateServerCertificate"
| "ChangePassword"
| "ResyncMFADevice"
| "SetSecurityTokenServicePreferences"
| "TagSAMLProvider"
| "UntagSAMLProvider"
| "TagServerCertificate"
| "UntagServerCertificate"
| "TagMFADevice"
| "UntagMFADevice"
| "CreateServiceSpecificCredential"
| "DeleteServiceSpecificCredential"
| "ResetServiceSpecificCredential"
| "UpdateServiceSpecificCredential"
| "EnableOrganizationsRootCredentialsManagement"
| "DisableOrganizationsRootCredentialsManagement"
| "EnableOrganizationsRootSessions"
| "DisableOrganizationsRootSessions"
| "GenerateOrganizationsAccessReport"
| "GenerateServiceLastAccessedDetails"
| "CreateDelegationRequest"
| "AcceptDelegationRequest"
| "RejectDelegationRequest"
| "UpdateDelegationRequest"
| "AssociateDelegationRequest"
| "SendDelegationToken"
| "EnableOutboundWebIdentityFederation"
| "DisableOutboundWebIdentityFederation"
)
}
pub(crate) fn iam_resource_tags(
state: &SharedIamState,
resource_arn: &str,
) -> Option<std::collections::HashMap<String, String>> {
if resource_arn == "*" {
return Some(std::collections::HashMap::new());
}
let parts: Vec<&str> = resource_arn.split(':').collect();
if parts.len() < 6 {
return None;
}
let resource = parts[5];
let account_id = parts.get(4).copied().unwrap_or("");
let accounts = state.read();
let state = accounts.get(account_id)?;
if let Some(rest) = resource.strip_prefix("user/") {
let name = rest.rsplit('/').next().unwrap_or(rest);
state
.users
.get(name)
.map(|u| tags_to_hashmap_or_empty(&u.tags))
} else if let Some(rest) = resource.strip_prefix("role/") {
let name = rest.rsplit('/').next().unwrap_or(rest);
state
.roles
.get(name)
.map(|r| tags_to_hashmap_or_empty(&r.tags))
} else if resource.starts_with("policy/") {
state
.policies
.get(resource_arn)
.map(|p| tags_to_hashmap_or_empty(&p.tags))
} else if let Some(rest) = resource.strip_prefix("instance-profile/") {
let name = rest.rsplit('/').next().unwrap_or(rest);
state
.instance_profiles
.get(name)
.map(|ip| tags_to_hashmap_or_empty(&ip.tags))
} else {
Some(std::collections::HashMap::new())
}
}
pub(crate) fn tags_to_hashmap_or_empty(tags: &[Tag]) -> std::collections::HashMap<String, String> {
tags.iter()
.map(|t| (t.key.clone(), t.value.clone()))
.collect()
}
pub(crate) fn iam_request_tags(
request: &AwsRequest,
action: &str,
) -> Option<std::collections::HashMap<String, String>> {
const TAG_ACTIONS: &[&str] = &[
"CreateUser",
"TagUser",
"CreateRole",
"TagRole",
"CreatePolicy",
"TagPolicy",
"CreateInstanceProfile",
"TagInstanceProfile",
"CreateOpenIDConnectProvider",
"TagOpenIDConnectProvider",
"CreateSAMLProvider",
"TagSAMLProvider",
"UploadServerCertificate",
"TagServerCertificate",
];
if TAG_ACTIONS.contains(&action) {
let tags = parse_tags(&request.query_params);
Some(tags.into_iter().map(|t| (t.key, t.value)).collect())
} else {
Some(std::collections::HashMap::new())
}
}
pub(crate) fn iam_action_resource(
action: &str,
partition: &str,
account: &str,
request: &AwsRequest,
) -> String {
let params = &request.query_params;
let wildcard = || "*".to_string();
let user_arn = |name: &str| format!("arn:{}:iam::{}:user/{}", partition, account, name);
let role_arn = |name: &str| format!("arn:{}:iam::{}:role/{}", partition, account, name);
let group_arn = |name: &str| format!("arn:{}:iam::{}:group/{}", partition, account, name);
let policy_arn = |name: &str| format!("arn:{}:iam::{}:policy/{}", partition, account, name);
let profile_arn = |name: &str| {
format!(
"arn:{}:iam::{}:instance-profile/{}",
partition, account, name
)
};
let mfa_arn = |name: &str| format!("arn:{}:iam::{}:mfa/{}", partition, account, name);
let server_cert_arn = |name: &str| {
format!(
"arn:{}:iam::{}:server-certificate/{}",
partition, account, name
)
};
let user_scoped: &[&str] = &[
"CreateUser",
"GetUser",
"DeleteUser",
"UpdateUser",
"TagUser",
"UntagUser",
"ListUserTags",
"CreateAccessKey",
"DeleteAccessKey",
"ListAccessKeys",
"UpdateAccessKey",
"GetAccessKeyLastUsed",
"CreateLoginProfile",
"GetLoginProfile",
"DeleteLoginProfile",
"UpdateLoginProfile",
"AttachUserPolicy",
"DetachUserPolicy",
"ListAttachedUserPolicies",
"PutUserPolicy",
"GetUserPolicy",
"DeleteUserPolicy",
"ListUserPolicies",
"PutUserPermissionsBoundary",
"DeleteUserPermissionsBoundary",
"AddUserToGroup",
"RemoveUserFromGroup",
"ListGroupsForUser",
"EnableMFADevice",
"DeactivateMFADevice",
"ListMFADevices",
"UploadSSHPublicKey",
"GetSSHPublicKey",
"UpdateSSHPublicKey",
"DeleteSSHPublicKey",
"ListSSHPublicKeys",
"UploadSigningCertificate",
"UpdateSigningCertificate",
"DeleteSigningCertificate",
"ListSigningCertificates",
];
let role_scoped: &[&str] = &[
"CreateRole",
"GetRole",
"DeleteRole",
"UpdateRole",
"UpdateRoleDescription",
"UpdateAssumeRolePolicy",
"TagRole",
"UntagRole",
"ListRoleTags",
"PutRolePermissionsBoundary",
"DeleteRolePermissionsBoundary",
"AttachRolePolicy",
"DetachRolePolicy",
"ListAttachedRolePolicies",
"PutRolePolicy",
"GetRolePolicy",
"DeleteRolePolicy",
"ListRolePolicies",
"DeleteServiceLinkedRole",
"ListInstanceProfilesForRole",
];
let group_scoped: &[&str] = &[
"CreateGroup",
"GetGroup",
"DeleteGroup",
"UpdateGroup",
"PutGroupPolicy",
"GetGroupPolicy",
"DeleteGroupPolicy",
"ListGroupPolicies",
"AttachGroupPolicy",
"DetachGroupPolicy",
"ListAttachedGroupPolicies",
];
let policy_scoped_arn: &[&str] = &[
"GetPolicy",
"DeletePolicy",
"TagPolicy",
"UntagPolicy",
"ListPolicyTags",
"CreatePolicyVersion",
"GetPolicyVersion",
"ListPolicyVersions",
"DeletePolicyVersion",
"SetDefaultPolicyVersion",
"ListEntitiesForPolicy",
];
let profile_scoped: &[&str] = &[
"CreateInstanceProfile",
"GetInstanceProfile",
"DeleteInstanceProfile",
"TagInstanceProfile",
"UntagInstanceProfile",
"ListInstanceProfileTags",
"AddRoleToInstanceProfile",
"RemoveRoleFromInstanceProfile",
];
if user_scoped.contains(&action) {
return params
.get("UserName")
.map(|n| user_arn(n))
.unwrap_or_else(wildcard);
}
if role_scoped.contains(&action) {
return params
.get("RoleName")
.map(|n| role_arn(n))
.unwrap_or_else(wildcard);
}
if group_scoped.contains(&action) {
return params
.get("GroupName")
.map(|n| group_arn(n))
.unwrap_or_else(wildcard);
}
if policy_scoped_arn.contains(&action) {
return params.get("PolicyArn").cloned().unwrap_or_else(wildcard);
}
if profile_scoped.contains(&action) {
return params
.get("InstanceProfileName")
.map(|n| profile_arn(n))
.unwrap_or_else(wildcard);
}
match action {
"CreatePolicy" => params
.get("PolicyName")
.map(|n| policy_arn(n))
.unwrap_or_else(wildcard),
"CreateServiceLinkedRole" => params
.get("AWSServiceName")
.map(|svc| {
format!(
"arn:{}:iam::{}:role/aws-service-role/{}",
partition, account, svc
)
})
.unwrap_or_else(wildcard),
"CreateVirtualMFADevice" => params
.get("VirtualMFADeviceName")
.map(|n| mfa_arn(n))
.unwrap_or_else(wildcard),
"DeleteVirtualMFADevice" => params.get("SerialNumber").cloned().unwrap_or_else(wildcard),
"UploadServerCertificate" | "GetServerCertificate" | "DeleteServerCertificate" => params
.get("ServerCertificateName")
.map(|n| server_cert_arn(n))
.unwrap_or_else(wildcard),
"CreateSAMLProvider" => params
.get("Name")
.map(|n| format!("arn:{}:iam::{}:saml-provider/{}", partition, account, n))
.unwrap_or_else(wildcard),
"UpdateSAMLProvider" | "DeleteSAMLProvider" | "GetSAMLProvider" => params
.get("SAMLProviderArn")
.cloned()
.unwrap_or_else(wildcard),
"CreateOpenIDConnectProvider" => params
.get("Url")
.map(|u| {
format!(
"arn:{}:iam::{}:oidc-provider/{}",
partition,
account,
u.trim_start_matches("https://")
)
})
.unwrap_or_else(wildcard),
"GetOpenIDConnectProvider"
| "DeleteOpenIDConnectProvider"
| "AddClientIDToOpenIDConnectProvider"
| "RemoveClientIDFromOpenIDConnectProvider"
| "UpdateOpenIDConnectProviderThumbprint"
| "TagOpenIDConnectProvider"
| "UntagOpenIDConnectProvider"
| "ListOpenIDConnectProviderTags" => params
.get("OpenIDConnectProviderArn")
.cloned()
.unwrap_or_else(wildcard),
"ListUsers"
| "ListRoles"
| "ListGroups"
| "ListPolicies"
| "ListInstanceProfiles"
| "ListVirtualMFADevices"
| "ListServerCertificates"
| "ListSAMLProviders"
| "ListOpenIDConnectProviders"
| "ListAccountAliases"
| "CreateAccountAlias"
| "DeleteAccountAlias"
| "GetAccountSummary"
| "GetAccountAuthorizationDetails"
| "GenerateCredentialReport"
| "GetCredentialReport"
| "GetAccountPasswordPolicy"
| "UpdateAccountPasswordPolicy"
| "DeleteAccountPasswordPolicy" => wildcard(),
_ => wildcard(),
}
}
pub(crate) fn extract_access_key(req: &AwsRequest) -> Option<String> {
let auth = req.headers.get("authorization")?.to_str().ok()?;
let info = fakecloud_aws::sigv4::parse_sigv4(auth)?;
Some(info.access_key)
}
pub(crate) fn title_case_service(s: &str) -> String {
s.split('-')
.map(|w| {
match w {
"autoscaling" => "AutoScaling".to_string(),
"loadbalancing" => "LoadBalancing".to_string(),
"mapreduce" => "MapReduce".to_string(),
"beanstalk" => "Beanstalk".to_string(),
_ => {
let mut c = w.chars();
match c.next() {
None => String::new(),
Some(ch) => ch.to_uppercase().to_string() + c.as_str(),
}
}
}
})
.collect::<String>()
}
pub(crate) fn url_encode(s: &str) -> String {
use std::fmt::Write;
let mut result = String::new();
for byte in s.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
result.push(byte as char);
}
_ => {
write!(result, "%{:02X}", byte).unwrap();
}
}
}
result
}
pub(crate) fn resolve_calling_user(state: &crate::state::IamState, _account_id: &str) -> String {
state
.users
.keys()
.next()
.cloned()
.unwrap_or_else(|| "default".to_string())
}
pub(crate) fn generate_id() -> String {
uuid::Uuid::new_v4()
.to_string()
.replace('-', "")
.to_uppercase()[..16]
.to_string()
}
pub(crate) fn generate_long_id() -> String {
uuid::Uuid::new_v4()
.to_string()
.replace('-', "")
.to_uppercase()[..21]
.to_string()
}
pub(crate) fn parse_tags(params: &std::collections::HashMap<String, String>) -> Vec<Tag> {
let mut tags = Vec::new();
let mut i = 1;
loop {
let key_param = format!("Tags.member.{i}.Key");
let value_param = format!("Tags.member.{i}.Value");
match params.get(&key_param) {
Some(key) => {
let value = params.get(&value_param).cloned().unwrap_or_default();
tags.push(Tag {
key: key.clone(),
value,
});
i += 1;
}
None => break,
}
}
tags
}
pub(crate) fn parse_tag_keys(params: &std::collections::HashMap<String, String>) -> Vec<String> {
let mut keys = Vec::new();
let mut i = 1;
loop {
let key_param = format!("TagKeys.member.{i}");
match params.get(&key_param) {
Some(key) => {
keys.push(key.clone());
i += 1;
}
None => break,
}
}
keys
}
pub(crate) fn tags_xml(tags: &[Tag]) -> String {
tags.iter()
.map(|t| {
format!(
" <member>\n <Key>{}</Key>\n <Value>{}</Value>\n </member>",
xml_escape(&t.key),
xml_escape(&t.value)
)
})
.collect::<Vec<_>>()
.join("\n")
}
pub(crate) fn paginated_tags_response(
action: &str,
tags: &[Tag],
req: &AwsRequest,
) -> Result<String, AwsServiceError> {
let max_items_i64 = parse_optional_i64_param(
"maxItems",
req.query_params.get("MaxItems").map(|s| s.as_str()),
)?;
validate_optional_range_i64("maxItems", max_items_i64, 1, 1000)?;
let max_items: usize = max_items_i64.unwrap_or(100) as usize;
let next_token = req.query_params.get("Marker").map(|s| s.as_str());
let (page, next_marker) = paginate(tags, next_token, max_items);
let is_truncated = next_marker.is_some();
let members = tags_xml(&page);
let marker = match &next_marker {
Some(m) => format!("<Marker>{m}</Marker>"),
None => String::new(),
};
Ok(format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<{action}Response xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<{action}Result>
<IsTruncated>{is_truncated}</IsTruncated>
<Tags>
{members}
</Tags>
{marker}
</{action}Result>
<ResponseMetadata>
<RequestId>{}</RequestId>
</ResponseMetadata>
</{action}Response>"#,
req.request_id
))
}
pub(crate) fn validate_tags(tags: &[Tag], existing_count: usize) -> Result<(), AwsServiceError> {
if tags.len() + existing_count > 50 {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"InvalidInput",
"1 validation error detected: Value at 'tags' failed to satisfy constraint: Member must have length less than or equal to 50.".to_string(),
));
}
let mut seen_keys = std::collections::HashSet::new();
for tag in tags {
let lower = tag.key.to_lowercase();
if !seen_keys.insert(lower) {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"InvalidInput",
"Duplicate tag keys found. Please note that Tag keys are case insensitive."
.to_string(),
));
}
if tag.key.len() > 128 {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"InvalidInput",
format!(
"1 validation error detected: Value at 'tags.{}.member.key' failed to satisfy constraint: Member must have length less than or equal to 128.",
seen_keys.len()
),
));
}
if tag.value.len() > 256 {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"InvalidInput",
format!(
"1 validation error detected: Value at 'tags.{}.member.value' failed to satisfy constraint: Member must have length less than or equal to 256.",
seen_keys.len()
),
));
}
if !tag.key.chars().all(|c| {
c.is_alphanumeric()
|| c == ' '
|| c == '+'
|| c == '-'
|| c == '='
|| c == '.'
|| c == '_'
|| c == ':'
|| c == '/'
|| c == '@'
}) {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"InvalidInput",
format!(
"1 validation error detected: Value at 'tags.{}.member.key' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\p{{L}}\\p{{Z}}\\p{{N}}_.:/=+\\-@]+",
seen_keys.len()
),
));
}
}
Ok(())
}
pub(crate) fn validate_untag_keys(keys: &[String]) -> Result<(), AwsServiceError> {
if keys.len() > 50 {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"ValidationError",
"1 validation error detected: Value at 'tagKeys' failed to satisfy constraint: Member must have length less than or equal to 50.".to_string(),
));
}
for key in keys {
if key.len() > 128 {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"ValidationError",
"1 validation error detected: Value at 'tagKeys' failed to satisfy constraint: Member must have length less than or equal to 128.".to_string(),
));
}
if !key.chars().all(|c| {
c.is_alphanumeric()
|| c == ' '
|| c == '+'
|| c == '-'
|| c == '='
|| c == '.'
|| c == '_'
|| c == ':'
|| c == '/'
|| c == '@'
}) {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"ValidationError",
"1 validation error detected: Value at 'tagKeys' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]+".to_string(),
));
}
}
Ok(())
}
pub(crate) fn attached_policy_name(state: &crate::state::IamState, arn: &str) -> String {
state
.policies
.get(arn)
.map(|p| p.policy_name.clone())
.unwrap_or_else(|| arn.rsplit('/').next().unwrap_or(arn).to_string())
}
pub(crate) fn required_param_with_code(
params: &std::collections::HashMap<String, String>,
name: &str,
code: &str,
) -> Result<String, AwsServiceError> {
params
.get(name)
.cloned()
.filter(|v| !v.is_empty())
.ok_or_else(|| {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
code,
format!("The request must contain the parameter {name}."),
)
})
}
pub(crate) fn validate_string_length_with_code(
field: &str,
value: &str,
min: usize,
max: usize,
code: &str,
) -> Result<(), AwsServiceError> {
let len = value.len();
if len < min || len > max {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
code,
format!(
"Value at '{field}' failed to satisfy constraint: \
Member must have length between {min} and {max}",
),
));
}
Ok(())
}
pub(crate) fn validate_optional_string_length_with_code(
field: &str,
value: Option<&str>,
min: usize,
max: usize,
code: &str,
) -> Result<(), AwsServiceError> {
if let Some(v) = value {
validate_string_length_with_code(field, v, min, max, code)?;
}
Ok(())
}
pub(crate) fn validate_list_pagination(req: &AwsRequest) -> Result<i64, AwsServiceError> {
validate_optional_string_length_with_code(
"Marker",
req.query_params.get("Marker").map(|s| s.as_str()),
1,
320,
"InvalidInput",
)?;
validate_optional_string_length_with_code(
"PathPrefix",
req.query_params.get("PathPrefix").map(|s| s.as_str()),
1,
512,
"InvalidInput",
)?;
let raw = req.query_params.get("MaxItems").map(|s| s.as_str());
let max_items_i64: Option<i64> = match raw {
Some(s) => Some(s.parse().map_err(|_| {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"InvalidInput",
format!("Value '{s}' at 'MaxItems' is not a valid integer"),
)
})?),
None => None,
};
if let Some(v) = max_items_i64 {
if !(1..=1000).contains(&v) {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"InvalidInput",
format!("Value '{v}' at 'MaxItems' must be between 1 and 1000"),
));
}
}
Ok(max_items_i64.unwrap_or(100))
}
pub(crate) fn empty_response(action: &str, request_id: &str) -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<{action}Response xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<{action}Result/>
<ResponseMetadata>
<RequestId>{request_id}</RequestId>
</ResponseMetadata>
</{action}Response>"#,
)
}