rusoto_sts/custom/
web_identity.rs

1use crate::custom::credential::NewAwsCredsForStsCreds;
2use crate::{AssumeRoleWithWebIdentityRequest, Sts, StsClient, PolicyDescriptorType};
3use rusoto_core::credential::{
4    AwsCredentials, CredentialsError, ProvideAwsCredentials, Secret, Variable,
5};
6use rusoto_core::request::HttpClient;
7use rusoto_core::{Client, Region};
8
9use async_trait::async_trait;
10
11const AWS_WEB_IDENTITY_TOKEN_FILE: &str = "AWS_WEB_IDENTITY_TOKEN_FILE";
12
13const AWS_ROLE_ARN: &str = "AWS_ROLE_ARN";
14
15const AWS_ROLE_SESSION_NAME: &str = "AWS_ROLE_SESSION_NAME";
16
17/// WebIdentityProvider using OpenID Connect bearer token to retrieve AWS IAM credentials.
18///
19/// See https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html for
20/// more details.
21#[derive(Debug, Clone)]
22pub struct WebIdentityProvider {
23    /// The OAuth 2.0 access token or OpenID Connect ID token that is provided by the identity provider.
24    /// Your application must get this token by authenticating the user who is using your application
25    /// with a web identity provider before the application makes an AssumeRoleWithWebIdentity call.
26    pub web_identity_token: Variable<Secret, CredentialsError>,
27    /// The Amazon Resource Name (ARN) of the role that the caller is assuming.
28    pub role_arn: Variable<String, CredentialsError>,
29    /// An identifier for the assumed role session. Typically, you pass the name or identifier that is
30    /// associated with the user who is using your application. That way, the temporary security credentials
31    /// that your application will use are associated with that user. This session name is included as part
32    /// of the ARN and assumed role ID in the AssumedRoleUser response element.
33    pub role_session_name: Option<Variable<Option<String>, CredentialsError>>,
34
35    /// The duration, in seconds, of the role session.
36    /// The value can range from 900 seconds (15 minutes) up to the maximum session duration setting for the role.
37    pub duration_seconds: Option<i64>,
38    /// An IAM policy in JSON format that you want to use as an inline session policy.
39    pub policy: Option<String>,
40    /// The Amazon Resource Names (ARNs) of the IAM managed policies that you want to use as managed session policies.
41    pub policy_arns: Option<Vec<PolicyDescriptorType>>,
42}
43
44impl WebIdentityProvider {
45    /// Create new WebIdentityProvider by explicitly passing its configuration.
46    pub fn new<A, B, C>(web_identity_token: A, role_arn: B, role_session_name: Option<C>) -> Self
47    where
48        A: Into<Variable<Secret, CredentialsError>>,
49        B: Into<Variable<String, CredentialsError>>,
50        C: Into<Variable<Option<String>, CredentialsError>>,
51    {
52        Self {
53            web_identity_token: web_identity_token.into(),
54            role_arn: role_arn.into(),
55            role_session_name: role_session_name.map(|v| v.into()),
56            duration_seconds: None,
57            policy: None,
58            policy_arns: None
59        }
60    }
61
62    /// Creat a WebIdentityProvider from the following environment variables:
63    ///
64    /// - `AWS_WEB_IDENTITY_TOKEN_FILE` path to the web identity token file.
65    /// - `AWS_ROLE_ARN` ARN of the role to assume.
66    /// - `AWS_ROLE_SESSION_NAME` (optional) name applied to the assume-role session.
67    ///
68    /// See https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-technical-overview.html
69    /// for more information about how IAM Roles for Kubernetes Service Accounts works.
70    pub fn from_k8s_env() -> Self {
71        Self::_from_k8s_env(
72            Variable::from_env_var(AWS_WEB_IDENTITY_TOKEN_FILE),
73            Variable::from_env_var(AWS_ROLE_ARN),
74            Variable::from_env_var_optional(AWS_ROLE_SESSION_NAME),
75        )
76    }
77
78    /// Used by unit testing
79    pub(crate) fn _from_k8s_env(
80        token_file: Variable<String, CredentialsError>,
81        role: Variable<String, CredentialsError>,
82        session_name: Variable<Option<String>, CredentialsError>,
83    ) -> Self {
84        Self::new(
85            Variable::dynamic(move || Variable::from_text_file(token_file.resolve()?).resolve()),
86            role,
87            Some(session_name),
88        )
89    }
90
91    #[cfg(test)]
92    pub(crate) fn load_token(&self) -> Result<Secret, CredentialsError> {
93        self.web_identity_token.resolve()
94    }
95
96    fn create_session_name() -> String {
97        // TODO can we do better here?
98        // - Pod service account, Pod name and Pod namespace
99        // - EC2 Instance ID if available
100        // - IP address if available
101        // - ...
102        // Having some information in the session name that identifies the client would enable
103        // better correlation analysis in CloudTrail.
104        "WebIdentitySession".to_string()
105    }
106}
107
108#[async_trait]
109impl ProvideAwsCredentials for WebIdentityProvider {
110    async fn credentials(&self) -> Result<AwsCredentials, CredentialsError> {
111        let http_client = match HttpClient::new() {
112            Ok(c) => c,
113            Err(e) => return Err(CredentialsError::new(e)),
114        };
115        let client = Client::new_not_signing(http_client);
116        let sts = StsClient::new_with_client(client, Region::default());
117        let mut req = AssumeRoleWithWebIdentityRequest::default();
118
119        req.role_arn = self.role_arn.resolve()?;
120        req.web_identity_token = self.web_identity_token.resolve()?.as_ref().to_string();
121        req.policy = self.policy.to_owned();
122        req.duration_seconds = self.duration_seconds.to_owned();
123        req.policy_arns = self.policy_arns.to_owned();
124        req.role_session_name = match self.role_session_name {
125            Some(ref role_session_name) => match role_session_name.resolve()? {
126                Some(session_name) => session_name,
127                None => Self::create_session_name(),
128            },
129            None => Self::create_session_name(),
130        };
131
132        let assume_role = sts.assume_role_with_web_identity(req).await;
133        match assume_role {
134            Err(e) => Err(CredentialsError::new(e)),
135            Ok(role) => match role.credentials {
136                None => Err(CredentialsError::new(format!(
137                    "No credentials found in AssumeRoleWithWebIdentityResponse: {:?}",
138                    role
139                ))),
140                Some(c) => AwsCredentials::new_for_credentials(c),
141            },
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use std::io::Write;
150    use tempfile::NamedTempFile;
151
152    #[test]
153    fn api_ergonomy() {
154        WebIdentityProvider::new(Secret::from("".to_string()), "", Some(Some("".to_string())));
155    }
156
157    #[test]
158    fn from_k8s_env() -> Result<(), CredentialsError> {
159        const TOKEN_VALUE: &str = "secret";
160        const ROLE_ARN: &str = "role";
161        const SESSION_NAME: &str = "session";
162        let mut file = NamedTempFile::new()?;
163        // We use writeln to add an extra newline at the end of the token, which should be
164        // removed by Variable::from_text_file.
165        writeln!(file, "{}", TOKEN_VALUE)?;
166        let p = WebIdentityProvider::_from_k8s_env(
167            Variable::with_value(file.path().to_string_lossy().to_string()),
168            Variable::with_value(ROLE_ARN.to_string()),
169            Variable::with_value(SESSION_NAME.to_string()),
170        );
171        let token = p.load_token()?;
172        assert_eq!(token.as_ref(), TOKEN_VALUE);
173        Ok(())
174    }
175}