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
43fn parse_sts_params(params: &[(String, String)]) -> Result<StsRequest, ProxyError> {
44    let role_arn = params
45        .iter()
46        .find(|(k, _)| k == "RoleArn")
47        .map(|(_, v)| v.clone())
48        .ok_or_else(|| ProxyError::InvalidRequest("missing RoleArn".into()))?;
49
50    let web_identity_token = params
51        .iter()
52        .find(|(k, _)| k == "WebIdentityToken")
53        .map(|(_, v)| v.clone())
54        .ok_or_else(|| ProxyError::InvalidRequest("missing WebIdentityToken".into()))?;
55
56    let duration_seconds = params
57        .iter()
58        .find(|(k, _)| k == "DurationSeconds")
59        .and_then(|(_, v)| v.parse().ok());
60
61    Ok(StsRequest {
62        role_arn,
63        web_identity_token,
64        duration_seconds,
65    })
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_not_sts_request() {
74        assert!(try_parse_sts_request(None).is_none());
75        assert!(try_parse_sts_request(Some("prefix=foo/")).is_none());
76        assert!(try_parse_sts_request(Some("Action=ListBuckets")).is_none());
77    }
78
79    #[test]
80    fn test_valid_sts_request() {
81        let query = "Action=AssumeRoleWithWebIdentity&RoleArn=my-role&WebIdentityToken=tok123";
82        let result = try_parse_sts_request(Some(query)).unwrap().unwrap();
83        assert_eq!(result.role_arn, "my-role");
84        assert_eq!(result.web_identity_token, "tok123");
85        assert_eq!(result.duration_seconds, None);
86    }
87
88    #[test]
89    fn test_sts_request_with_duration() {
90        let query =
91            "Action=AssumeRoleWithWebIdentity&RoleArn=r&WebIdentityToken=t&DurationSeconds=7200";
92        let result = try_parse_sts_request(Some(query)).unwrap().unwrap();
93        assert_eq!(result.duration_seconds, Some(7200));
94    }
95
96    #[test]
97    fn test_missing_role_arn() {
98        let query = "Action=AssumeRoleWithWebIdentity&WebIdentityToken=tok";
99        let result = try_parse_sts_request(Some(query)).unwrap();
100        assert!(result.is_err());
101    }
102
103    #[test]
104    fn test_missing_web_identity_token() {
105        let query = "Action=AssumeRoleWithWebIdentity&RoleArn=role";
106        let result = try_parse_sts_request(Some(query)).unwrap();
107        assert!(result.is_err());
108    }
109}