use multistore::error::ProxyError;
use multistore::types::TemporaryCredentials;
use quick_xml::se::to_string as xml_to_string;
use serde::Serialize;
#[derive(Debug, Serialize)]
#[serde(rename = "AssumeRoleWithWebIdentityResponse")]
pub struct AssumeRoleWithWebIdentityResponse {
#[serde(rename = "AssumeRoleWithWebIdentityResult")]
pub result: AssumeRoleWithWebIdentityResult,
}
#[derive(Debug, Serialize)]
pub struct AssumeRoleWithWebIdentityResult {
#[serde(rename = "Credentials")]
pub credentials: StsCredentials,
#[serde(rename = "AssumedRoleUser")]
pub assumed_role_user: AssumedRoleUser,
}
#[derive(Debug, Serialize)]
pub struct StsCredentials {
#[serde(rename = "AccessKeyId")]
pub access_key_id: String,
#[serde(rename = "SecretAccessKey")]
pub secret_access_key: String,
#[serde(rename = "SessionToken")]
pub session_token: String,
#[serde(rename = "Expiration")]
pub expiration: String,
}
#[derive(Debug, Serialize)]
pub struct AssumedRoleUser {
#[serde(rename = "AssumedRoleId")]
pub assumed_role_id: String,
#[serde(rename = "Arn")]
pub arn: String,
}
impl AssumeRoleWithWebIdentityResponse {
pub fn to_xml(&self) -> String {
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n{}",
xml_to_string(self).unwrap_or_default()
)
}
}
pub fn build_sts_response(creds: &TemporaryCredentials) -> (u16, String) {
let response = AssumeRoleWithWebIdentityResponse {
result: AssumeRoleWithWebIdentityResult {
credentials: StsCredentials {
access_key_id: creds.access_key_id.clone(),
secret_access_key: creds.secret_access_key.clone(),
session_token: creds.session_token.clone(),
expiration: creds.expiration.to_rfc3339(),
},
assumed_role_user: AssumedRoleUser {
assumed_role_id: creds.assumed_role_id.clone(),
arn: creds.assumed_role_id.clone(),
},
},
};
(200, response.to_xml())
}
pub const SYNTHETIC_ACCOUNT_ID: &str = "000000000000";
#[derive(Debug, Serialize)]
#[serde(rename = "GetCallerIdentityResponse")]
struct GetCallerIdentityResponse {
#[serde(rename = "GetCallerIdentityResult")]
result: GetCallerIdentityResult,
}
#[derive(Debug, Serialize)]
struct GetCallerIdentityResult {
#[serde(rename = "Arn")]
arn: String,
#[serde(rename = "UserId")]
user_id: String,
#[serde(rename = "Account")]
account: String,
}
pub fn build_caller_identity_response(creds: &TemporaryCredentials) -> String {
let response = GetCallerIdentityResponse {
result: GetCallerIdentityResult {
arn: format!(
"arn:aws:sts::{}:assumed-role/{}/{}",
SYNTHETIC_ACCOUNT_ID, creds.assumed_role_id, creds.source_identity
),
user_id: format!("{}:{}", creds.assumed_role_id, creds.source_identity),
account: SYNTHETIC_ACCOUNT_ID.to_string(),
},
};
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n{}",
xml_to_string(&response).unwrap_or_default()
)
}
pub fn build_sts_error_response(err: &ProxyError) -> (u16, String) {
let (status, code, message) = match err {
ProxyError::RoleNotFound(r) => (
400,
"MalformedPolicyDocument",
format!("role not found: {}", r),
),
ProxyError::InvalidOidcToken(msg) => (400, "InvalidIdentityToken", msg.clone()),
ProxyError::InvalidRequest(msg) => (400, "InvalidParameterValue", msg.clone()),
ProxyError::AccessDenied => (403, "AccessDenied", "access denied".to_string()),
ProxyError::SignatureDoesNotMatch => (
403,
"SignatureDoesNotMatch",
"the request signature does not match".to_string(),
),
ProxyError::ExpiredCredentials => (
403,
"ExpiredToken",
"the security token included in the request is expired".to_string(),
),
_ => (500, "InternalError", "internal error".to_string()),
};
let xml = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<ErrorResponse>\
<Error>\
<Code>{}</Code>\
<Message>{}</Message>\
</Error>\
</ErrorResponse>",
code, message
);
(status, xml)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
fn creds() -> TemporaryCredentials {
TemporaryCredentials {
access_key_id: "STSPRXYAAA".into(),
secret_access_key: "secret".into(),
session_token: "sealed".into(),
expiration: Utc::now(),
allowed_scopes: vec![],
assumed_role_id: "github-actions".into(),
source_identity: "repo:org/repo:ref:refs/heads/main".into(),
}
}
#[test]
fn caller_identity_xml_has_account_arn_userid() {
let xml = build_caller_identity_response(&creds());
assert!(xml.contains("<Account>000000000000</Account>"), "{xml}");
assert!(xml.contains(
"<Arn>arn:aws:sts::000000000000:assumed-role/github-actions/repo:org/repo:ref:refs/heads/main</Arn>"
), "{xml}");
assert!(
xml.contains("<UserId>github-actions:repo:org/repo:ref:refs/heads/main</UserId>"),
"{xml}"
);
}
}