Skip to main content

multistore_sts/
request.rs

1//! STS request parsing.
2//!
3//! Extracts `AssumeRoleWithWebIdentity` parameters from a form-urlencoded
4//! parameter string — a query string, or the body of an
5//! `application/x-www-form-urlencoded` `POST` (the shape AWS SDKs send);
6//! `try_handle_sts` checks both.
7
8use multistore::error::ProxyError;
9
10/// Parsed STS `AssumeRoleWithWebIdentity` request parameters.
11#[derive(Debug, Clone)]
12pub struct StsRequest {
13    /// The ARN of the IAM role to assume.
14    pub role_arn: String,
15    /// The OIDC identity token provided by the caller.
16    pub web_identity_token: String,
17    /// Optional session duration in seconds.
18    pub duration_seconds: Option<u64>,
19}
20
21/// Try to parse an STS request from a form-urlencoded parameter string — a
22/// query string, or a form-encoded `POST` body (the two places AWS STS accepts
23/// parameters).
24///
25/// Returns `None` if the string does not contain `Action=AssumeRoleWithWebIdentity`
26/// (i.e., this is not an STS request). Returns `Some(Ok(..))` on success or
27/// `Some(Err(..))` if it is an STS request but required parameters are missing.
28pub fn try_parse_sts_request(query: Option<&str>) -> Option<Result<StsRequest, ProxyError>> {
29    let q = query?;
30    let params: Vec<(String, String)> = url::form_urlencoded::parse(q.as_bytes())
31        .map(|(k, v)| (k.to_string(), v.to_string()))
32        .collect();
33
34    let action = params.iter().find(|(k, _)| k == "Action");
35    match action {
36        Some((_, value)) if value == "AssumeRoleWithWebIdentity" => {}
37        _ => return None,
38    }
39
40    Some(parse_sts_params(&params))
41}
42
43/// Whether a form-urlencoded parameter string (query or `POST` body) requests
44/// `Action=GetCallerIdentity`.
45///
46/// `GetCallerIdentity` carries no other parameters worth extracting — it is
47/// authenticated entirely by the request's SigV4 signature — so a boolean is
48/// all the caller needs.
49pub fn is_get_caller_identity(params: Option<&str>) -> bool {
50    let Some(q) = params else { return false };
51    url::form_urlencoded::parse(q.as_bytes())
52        .any(|(k, v)| k == "Action" && v == "GetCallerIdentity")
53}
54
55fn parse_sts_params(params: &[(String, String)]) -> Result<StsRequest, ProxyError> {
56    let role_arn = params
57        .iter()
58        .find(|(k, _)| k == "RoleArn")
59        .map(|(_, v)| v.clone())
60        .ok_or_else(|| ProxyError::InvalidRequest("missing RoleArn".into()))?;
61
62    let web_identity_token = params
63        .iter()
64        .find(|(k, _)| k == "WebIdentityToken")
65        .map(|(_, v)| v.clone())
66        .ok_or_else(|| ProxyError::InvalidRequest("missing WebIdentityToken".into()))?;
67
68    let duration_seconds = params
69        .iter()
70        .find(|(k, _)| k == "DurationSeconds")
71        .and_then(|(_, v)| v.parse().ok());
72
73    Ok(StsRequest {
74        role_arn,
75        web_identity_token,
76        duration_seconds,
77    })
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_not_sts_request() {
86        assert!(try_parse_sts_request(None).is_none());
87        assert!(try_parse_sts_request(Some("prefix=foo/")).is_none());
88        assert!(try_parse_sts_request(Some("Action=ListBuckets")).is_none());
89    }
90
91    #[test]
92    fn test_valid_sts_request() {
93        let query = "Action=AssumeRoleWithWebIdentity&RoleArn=my-role&WebIdentityToken=tok123";
94        let result = try_parse_sts_request(Some(query)).unwrap().unwrap();
95        assert_eq!(result.role_arn, "my-role");
96        assert_eq!(result.web_identity_token, "tok123");
97        assert_eq!(result.duration_seconds, None);
98    }
99
100    #[test]
101    fn test_sts_request_with_duration() {
102        let query =
103            "Action=AssumeRoleWithWebIdentity&RoleArn=r&WebIdentityToken=t&DurationSeconds=7200";
104        let result = try_parse_sts_request(Some(query)).unwrap().unwrap();
105        assert_eq!(result.duration_seconds, Some(7200));
106    }
107
108    #[test]
109    fn test_missing_role_arn() {
110        let query = "Action=AssumeRoleWithWebIdentity&WebIdentityToken=tok";
111        let result = try_parse_sts_request(Some(query)).unwrap();
112        assert!(result.is_err());
113    }
114
115    #[test]
116    fn test_missing_web_identity_token() {
117        let query = "Action=AssumeRoleWithWebIdentity&RoleArn=role";
118        let result = try_parse_sts_request(Some(query)).unwrap();
119        assert!(result.is_err());
120    }
121
122    #[test]
123    fn test_is_get_caller_identity() {
124        assert!(is_get_caller_identity(Some(
125            "Action=GetCallerIdentity&Version=2011-06-15"
126        )));
127        // Order-independent and tolerant of extra params.
128        assert!(is_get_caller_identity(Some(
129            "Version=2011-06-15&Action=GetCallerIdentity"
130        )));
131        assert!(!is_get_caller_identity(Some(
132            "Action=AssumeRoleWithWebIdentity&RoleArn=r&WebIdentityToken=t"
133        )));
134        assert!(!is_get_caller_identity(Some("prefix=foo/")));
135        assert!(!is_get_caller_identity(None));
136    }
137}