Skip to main content

rustack_sts_http/
request.rs

1//! STS `awsQuery` request parameter parsing utilities.
2//!
3//! STS uses `application/x-www-form-urlencoded` request bodies with
4//! dot-notation for nested parameters (e.g., `Tags.member.1.Key`).
5
6use rustack_sts_model::error::StsError;
7
8/// Parse a URL-encoded body into a list of key-value pairs.
9#[must_use]
10pub fn parse_form_params(body: &[u8]) -> Vec<(String, String)> {
11    form_urlencoded::parse(body)
12        .map(|(k, v)| (k.into_owned(), v.into_owned()))
13        .collect()
14}
15
16/// Get a required parameter value.
17///
18/// Returns an error if the parameter is not present.
19pub fn get_required_param<'a>(
20    params: &'a [(String, String)],
21    key: &str,
22) -> Result<&'a str, StsError> {
23    params
24        .iter()
25        .find(|(k, _)| k == key)
26        .map(|(_, v)| v.as_str())
27        .ok_or_else(|| {
28            StsError::invalid_parameter_value(format!("Missing required parameter: {key}"))
29        })
30}
31
32/// Get an optional parameter value.
33#[must_use]
34pub fn get_optional_param<'a>(params: &'a [(String, String)], key: &str) -> Option<&'a str> {
35    params
36        .iter()
37        .find(|(k, _)| k == key)
38        .map(|(_, v)| v.as_str())
39}
40
41/// Parse session tags from awsQuery form parameters.
42///
43/// Tags are encoded as:
44/// - `Tags.member.1.Key=Project`
45/// - `Tags.member.1.Value=MyProject`
46#[must_use]
47pub fn parse_session_tags(params: &[(String, String)]) -> Vec<rustack_sts_model::types::Tag> {
48    let mut tags = Vec::new();
49    let mut index = 1;
50
51    loop {
52        let key_param = format!("Tags.member.{index}.Key");
53        let value_param = format!("Tags.member.{index}.Value");
54
55        let key = params
56            .iter()
57            .find(|(k, _)| k == &key_param)
58            .map(|(_, v)| v.clone());
59        let value = params
60            .iter()
61            .find(|(k, _)| k == &value_param)
62            .map(|(_, v)| v.clone());
63
64        match (key, value) {
65            (Some(k), Some(v)) => {
66                tags.push(rustack_sts_model::types::Tag { key: k, value: v });
67                index += 1;
68            }
69            (Some(k), None) => {
70                tags.push(rustack_sts_model::types::Tag {
71                    key: k,
72                    value: String::new(),
73                });
74                index += 1;
75            }
76            _ => break,
77        }
78    }
79
80    tags
81}
82
83/// Parse transitive tag keys from awsQuery form parameters.
84///
85/// Encoded as:
86/// - `TransitiveTagKeys.member.1=Project`
87/// - `TransitiveTagKeys.member.2=Env`
88#[must_use]
89pub fn parse_transitive_tag_keys(params: &[(String, String)]) -> Vec<String> {
90    let mut keys = Vec::new();
91    let mut index = 1;
92
93    loop {
94        let param = format!("TransitiveTagKeys.member.{index}");
95        match params.iter().find(|(k, _)| k == &param) {
96            Some((_, v)) => {
97                keys.push(v.clone());
98                index += 1;
99            }
100            None => break,
101        }
102    }
103
104    keys
105}
106
107/// Parse policy ARNs from awsQuery form parameters.
108#[must_use]
109pub fn parse_policy_arns(params: &[(String, String)]) -> Vec<String> {
110    let mut arns = Vec::new();
111    let mut index = 1;
112
113    loop {
114        let param = format!("PolicyArns.member.{index}.arn");
115        match params.iter().find(|(k, _)| k == &param) {
116            Some((_, v)) => {
117                arns.push(v.clone());
118                index += 1;
119            }
120            None => break,
121        }
122    }
123
124    arns
125}
126
127/// Extract the access key ID from a SigV4 Authorization header.
128///
129/// Parses the Credential component: `Credential=AKID/date/region/service/aws4_request`
130#[must_use]
131pub fn extract_access_key_from_auth(auth_header: &str) -> Option<String> {
132    let cred_start = auth_header.find("Credential=")?;
133    let cred_value = &auth_header[cred_start + 11..];
134    let cred_end = cred_value.find('/')?;
135    Some(cred_value[..cred_end].to_owned())
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_should_parse_form_params() {
144        let body = b"Action=GetCallerIdentity&Version=2011-06-15";
145        let params = parse_form_params(body);
146        assert_eq!(params.len(), 2);
147        assert_eq!(
148            params[0],
149            ("Action".to_owned(), "GetCallerIdentity".to_owned())
150        );
151    }
152
153    #[test]
154    fn test_should_get_required_param() {
155        let params = vec![("RoleArn".to_owned(), "arn:aws:iam::123:role/R".to_owned())];
156        assert_eq!(
157            get_required_param(&params, "RoleArn").unwrap(),
158            "arn:aws:iam::123:role/R"
159        );
160    }
161
162    #[test]
163    fn test_should_error_on_missing_required_param() {
164        let params: Vec<(String, String)> = vec![];
165        let err = get_required_param(&params, "RoleArn").unwrap_err();
166        assert!(err.message.contains("RoleArn"));
167    }
168
169    #[test]
170    fn test_should_get_optional_param() {
171        let params = vec![("DurationSeconds".to_owned(), "3600".to_owned())];
172        assert_eq!(get_optional_param(&params, "DurationSeconds"), Some("3600"));
173        assert_eq!(get_optional_param(&params, "Missing"), None);
174    }
175
176    #[test]
177    fn test_should_parse_session_tags() {
178        let params = vec![
179            ("Tags.member.1.Key".to_owned(), "Project".to_owned()),
180            ("Tags.member.1.Value".to_owned(), "MyProject".to_owned()),
181            ("Tags.member.2.Key".to_owned(), "Env".to_owned()),
182            ("Tags.member.2.Value".to_owned(), "Dev".to_owned()),
183        ];
184        let tags = parse_session_tags(&params);
185        assert_eq!(tags.len(), 2);
186        assert_eq!(tags[0].key, "Project");
187        assert_eq!(tags[0].value, "MyProject");
188        assert_eq!(tags[1].key, "Env");
189        assert_eq!(tags[1].value, "Dev");
190    }
191
192    #[test]
193    fn test_should_parse_transitive_tag_keys() {
194        let params = vec![
195            (
196                "TransitiveTagKeys.member.1".to_owned(),
197                "Project".to_owned(),
198            ),
199            ("TransitiveTagKeys.member.2".to_owned(), "Env".to_owned()),
200        ];
201        let keys = parse_transitive_tag_keys(&params);
202        assert_eq!(keys, vec!["Project", "Env"]);
203    }
204
205    #[test]
206    fn test_should_parse_policy_arns() {
207        let params = vec![(
208            "PolicyArns.member.1.arn".to_owned(),
209            "arn:aws:iam::123:policy/P".to_owned(),
210        )];
211        let arns = parse_policy_arns(&params);
212        assert_eq!(arns, vec!["arn:aws:iam::123:policy/P"]);
213    }
214
215    #[test]
216    fn test_should_extract_access_key_from_auth() {
217        let auth = "AWS4-HMAC-SHA256 \
218                    Credential=AKIAIOSFODNN7EXAMPLE/20260319/us-east-1/sts/aws4_request, \
219                    SignedHeaders=content-type;host;x-amz-date, Signature=abc123";
220        assert_eq!(
221            extract_access_key_from_auth(auth),
222            Some("AKIAIOSFODNN7EXAMPLE".to_owned())
223        );
224    }
225
226    #[test]
227    fn test_should_return_none_for_missing_credential() {
228        assert_eq!(extract_access_key_from_auth("Bearer token123"), None);
229    }
230
231    #[test]
232    fn test_should_parse_url_encoded_special_chars() {
233        let body = b"Action=AssumeRole&RoleArn=arn%3Aaws%3Aiam%3A%3A123456789012%3Arole%2FTestRole";
234        let params = parse_form_params(body);
235        assert_eq!(params[1].1, "arn:aws:iam::123456789012:role/TestRole");
236    }
237}