1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
//! AWS Secrets Engine
//!
//! See the [documentation](https://www.vaultproject.io/api/secret/aws/index.html).
use crate::{Error, LeasedData, Response};

use async_trait::async_trait;
use reqwest::Method;
use serde::{Deserialize, Serialize};

/// Parameters for configuring the Root credentials for the AWS Secrets Engine
#[derive(Serialize, Debug, Eq, PartialEq)]
pub struct RootCredentials {
    /// Number of max retries the client should use for recoverable errors.
    /// The default (-1) falls back to the AWS SDK's default behavior.
    #[serde(default = "default_max_retries")]
    pub max_retries: i64,
    /// Specifies the AWS access key ID.
    pub access_key: String,
    /// Specifies the AWS secret access key.
    pub secret_key: String,
    /// Specifies the AWS region. If not set it will use the AWS_REGION env var, AWS_DEFAULT_REGION
    /// env var, or us-east-1 in that order.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub region: Option<String>,
    /// Specifies a custom HTTP IAM endpoint to use.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub iam_endpoint: Option<String>,
    /// Specifies a custom HTTP STS endpoint to use.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sts_endpoint: Option<String>,
}

/// Parameters for configuring the lease for the AWS Secrets Engine
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Lease {
    /// Specifies the lease value provided as a string duration with time suffix. "h" (hour) is
    /// the largest suffix.
    pub lease: String,
    /// Specifies the maximum lease value provided as a string duration with time suffix. "h"
    /// (hour) is the largest suffix.
    pub lease_max: String,
}

/// AWS Secrets Engine Role
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Default)]
pub struct Role {}

/// Request to Generate Credentials
#[derive(Serialize, Debug, Eq, PartialEq, Default)]
pub struct CredentialsRequest {
    /// The ARN of the role to assume if credential_type on the Vault role is assumed_role.
    /// Must match one of the allowed role ARNs in the Vault role. Optional if the Vault role
    /// only allows a single AWS role ARN; required otherwise.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub role_arn: Option<String>,
    /// Specifies the TTL for the use of the STS token. This is specified as a string with a
    /// duration suffix. Valid only when credential_type is assumed_role or federation_token.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ttl: Option<String>,
}

/// Credentials Returned from Vault
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Default)]
pub struct Credentials {
    /// AWS Access Key
    pub access_key: String,
    /// AWS Secret Key
    pub secret_key: String,
    /// AWS Security Token, if any
    #[serde(default)]
    pub security_token: Option<String>,
}

/// AWS Secrets Engine
///
/// See the [documentation](https://www.vaultproject.io/api/secret/aws/index.html).
#[async_trait]
pub trait Aws {
    /// Configure the Root IAM Credentials that Vault uses to communicate with AWS
    async fn configure_root(&self, path: &str, config: &RootCredentials)
        -> Result<Response, Error>;
    /// Rotate Root IAM Credentials
    ///
    /// See [warnings](https://www.vaultproject.io/api/secret/aws/index.html#rotate-root-iam-credentials)
    /// on Vault's documentation
    async fn rotate_root(&self, path: &str) -> Result<Response, Error>;
    /// Configures the lease for the AWS Secrets Engine
    async fn configure_lease(&self, path: &str, lease: &Lease) -> Result<Response, Error>;
    /// Reads the Lease for the AWS Secrets Engine
    async fn read_lease(&self, path: &str) -> Result<Lease, Error>;
    /// Create role
    async fn create_role(&self, path: &str, role: &Role) -> Result<Response, Error>;
    /// Update Role
    async fn update_role(&self, path: &str, role: &Role) -> Result<Response, Error> {
        self.create_role(path, role).await
    }
    /// Read Role
    async fn read_role(&self, path: &str, role: &str) -> Result<Role, Error>;
    /// List Roles
    async fn list_roles(&self, path: &str) -> Result<Vec<String>, Error>;
    /// Delete Role
    async fn delete_role(&self, path: &str, role: &str) -> Result<Response, Error>;
    /// Generate Credentials
    async fn generate_credentials(
        &self,
        path: &str,
        role: &str,
        request: &CredentialsRequest,
    ) -> Result<LeasedData<Credentials>, Error>;
}

#[async_trait]
impl<T> Aws for T
where
    T: crate::Vault + Send + Sync,
{
    async fn configure_root(
        &self,
        path: &str,
        config: &RootCredentials,
    ) -> Result<Response, Error> {
        let values = serde_json::to_value(config)?;
        let path = format!("{}/config/root", path);
        self.post(&path, &values, false).await
    }

    async fn rotate_root(&self, path: &str) -> Result<Response, Error> {
        let path = format!("{}/config/rotate-root", path);
        self.read(&path, Method::POST).await
    }

    async fn configure_lease(&self, path: &str, lease: &Lease) -> Result<Response, Error> {
        let values = serde_json::to_value(lease)?;
        let path = format!("{}/config/lease", path);
        self.post(&path, &values, false).await
    }

    async fn read_lease(&self, path: &str) -> Result<Lease, Error> {
        let path = format!("{}/config/lease", path);
        let data: Lease = self.get(&path).await?.data()?;
        Ok(data)
    }

    async fn create_role(&self, _path: &str, _role: &Role) -> Result<Response, Error> {
        unimplemented!()
    }

    async fn read_role(&self, _path: &str, _role: &str) -> Result<Role, Error> {
        unimplemented!()
    }

    async fn list_roles(&self, _path: &str) -> Result<Vec<String>, Error> {
        unimplemented!()
    }

    async fn delete_role(&self, _path: &str, _role: &str) -> Result<Response, Error> {
        unimplemented!()
    }

    async fn generate_credentials(
        &self,
        path: &str,
        role: &str,
        request: &CredentialsRequest,
    ) -> Result<LeasedData<Credentials>, Error> {
        let path = format!("{}/creds/{}", path, role);
        self.get_with_query(&path, request).await?.leased_data()
    }
}

#[allow(dead_code)]
const fn default_max_retries() -> i64 {
    -1
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sys::mounts::tests::Mount;

    #[tokio::test(threaded_scheduler)]
    async fn can_configure() {
        let client = crate::tests::vault_client();

        let path = crate::tests::uuid_prefix("aws");
        let engine = crate::sys::mounts::SecretEngine {
            path,
            r#type: "aws".to_string(),
            ..Default::default()
        };

        let mount = Mount::new(&client, &engine).await;
        let config = RootCredentials {
            max_retries: -1,
            access_key: "aaa".to_string(),
            secret_key: "aaa".to_string(),
            region: None,
            iam_endpoint: Some("http://aws_iam:5000".to_string()),
            sts_endpoint: Some("http://aws_sts:8000".to_string()),
        };

        let response = Aws::configure_root(&client, &mount.path, &config)
            .await
            .unwrap();
        assert!(response.ok().unwrap().is_none());

        let lease = Lease {
            lease: "1h".to_string(),
            lease_max: "24h".to_string(),
        };
        let response = Aws::configure_lease(&client, &mount.path, &lease)
            .await
            .unwrap();
        assert!(response.ok().unwrap().is_none());

        let actual_lease = Aws::read_lease(&client, &mount.path).await.unwrap();
        assert_eq!(actual_lease.lease, "1h0m0s");
        assert_eq!(actual_lease.lease_max, "24h0m0s");
    }
}