multistore_sts/
route_handler.rs1use crate::{try_handle_sts, JwksCache, TokenKey};
7use multistore::registry::CredentialRegistry;
8use multistore::route_handler::{ProxyResult, RequestInfo, RouteHandler, RouteHandlerFuture};
9use multistore::router::Router;
10
11struct StsHandler<C> {
13 config: C,
14 cache: JwksCache,
15 key: Option<TokenKey>,
16}
17
18impl<C: CredentialRegistry> RouteHandler for StsHandler<C> {
19 fn handle<'a>(&'a self, req: &'a RequestInfo<'a>) -> RouteHandlerFuture<'a> {
20 Box::pin(async move {
21 let (status, xml) = try_handle_sts(
22 req.query,
23 req.form_body,
24 &self.config,
25 &self.cache,
26 self.key.as_ref(),
27 )
28 .await?;
29 Some(ProxyResult::xml(status, xml))
30 })
31 }
32}
33
34pub trait StsRouterExt {
36 fn with_sts<C: CredentialRegistry + 'static>(
45 self,
46 path: &str,
47 config: C,
48 cache: JwksCache,
49 key: Option<TokenKey>,
50 ) -> Self;
51}
52
53impl StsRouterExt for Router {
54 fn with_sts<C: CredentialRegistry + 'static>(
55 self,
56 path: &str,
57 config: C,
58 cache: JwksCache,
59 key: Option<TokenKey>,
60 ) -> Self {
61 self.route(path, StsHandler { config, cache, key })
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use multistore::error::ProxyError;
69 use multistore::types::{RoleConfig, StoredCredential};
70
71 #[derive(Clone)]
73 struct EmptyRegistry;
74
75 impl CredentialRegistry for EmptyRegistry {
76 async fn get_credential(
77 &self,
78 _access_key_id: &str,
79 ) -> Result<Option<StoredCredential>, ProxyError> {
80 Ok(None)
81 }
82 async fn get_role(&self, _role_id: &str) -> Result<Option<RoleConfig>, ProxyError> {
83 Ok(None)
84 }
85 }
86
87 fn test_router() -> Router {
88 let cache = JwksCache::new(reqwest::Client::new(), std::time::Duration::from_secs(60));
89 Router::new().with_sts("/", EmptyRegistry, cache, None)
90 }
91
92 #[tokio::test]
93 async fn sts_query_on_root_path_is_handled() {
94 let router = test_router();
95 let headers = http::HeaderMap::new();
96 let req = RequestInfo::new(
97 &http::Method::GET,
98 "/",
99 Some("Action=AssumeRoleWithWebIdentity&RoleArn=test&WebIdentityToken=tok"),
100 &headers,
101 None,
102 );
103 assert!(
104 router.dispatch(&req).await.is_some(),
105 "STS request to / must be intercepted by the router"
106 );
107 }
108
109 #[tokio::test]
110 async fn non_sts_query_on_root_path_falls_through() {
111 let router = test_router();
112 let headers = http::HeaderMap::new();
113 let req = RequestInfo::new(&http::Method::GET, "/", Some("prefix=foo/"), &headers, None);
114 assert!(
115 router.dispatch(&req).await.is_none(),
116 "non-STS request to / must fall through"
117 );
118 }
119
120 #[tokio::test]
121 async fn sts_form_body_post_is_handled() {
122 let router = test_router();
125 let headers = http::HeaderMap::new();
126 let req = RequestInfo::new(&http::Method::POST, "/", None, &headers, None)
127 .with_form_body(Some(
128 "Action=AssumeRoleWithWebIdentity&Version=2011-06-15&RoleArn=test&WebIdentityToken=tok",
129 ));
130 assert!(
131 router.dispatch(&req).await.is_some(),
132 "STS form-body POST must be intercepted by the router"
133 );
134 }
135
136 #[tokio::test]
137 async fn non_sts_form_body_falls_through() {
138 let router = test_router();
139 let headers = http::HeaderMap::new();
140 let req = RequestInfo::new(&http::Method::POST, "/", None, &headers, None)
141 .with_form_body(Some("grant_type=client_credentials"));
142 assert!(
143 router.dispatch(&req).await.is_none(),
144 "non-STS form body must fall through"
145 );
146 }
147}