diff --git a/src/client.rs b/src/client.rs index 908ba3a..9d79c9f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -69,6 +69,13 @@ impl VaultApiUrl { self.at("/v1/sys/seal-status") } + fn token(&self, path: &str) -> VaultAuthTokenApiUrl { + VaultAuthTokenApiUrl { + url: self.clone(), + path: path.into() + } + } + // https://www.vaultproject.io/api-docs/secret/transit fn transit(&self, mount_path: &str, name: &str) -> String { self.at(format!("/v1/{}/keys/{}", mount_path, urlencoding::encode(name))) @@ -87,6 +94,23 @@ impl VaultApiUrl { } } +struct VaultAuthTokenApiUrl { + url: VaultApiUrl, + path: String +} + +impl VaultAuthTokenApiUrl { + + fn create(&self) -> String { + self.url.at(format!("/v1/auth/{}/create", self.path)) + } + + fn lookup_self(&self) -> String { + self.url.at(format!("/v1/auth/{}/lookup-self", self.path)) + } + +} + struct VaultAuthUserpassApiUrl { url: VaultApiUrl, path: String @@ -279,11 +303,80 @@ impl VaultAuthApi { } } + pub fn tokens(&self) -> VaultAuthTokensApi { + VaultAuthTokensApi::new(self.url.clone(), "token") + } + pub fn userpass(&self, path: &str) -> VaultAuthUserpassApi { VaultAuthUserpassApi::new(self.url.clone(), path) } } +pub struct VaultAuthTokensApi { + url: VaultApiUrl, + path: String +} + +impl VaultAuthTokensApi { + + fn new(url: VaultApiUrl, path: &str) -> Self { + Self { + url, + path: path.into() + } + } + + pub async fn create(&self, auth_token: &str, request: &VaultAuthTokenCreateRequest) -> Result { + let url = self.url.token(&self.path).create(); + + info!("Connecting to {}", url); + + let response = reqwest::Client::new() + .post(url) + .header(VAULT_TOKEN_HEADER, auth_token) + .json(request) + .send() + .await?; + + let status = response.status(); + + if status.is_server_error() || status.is_client_error() { + Err(read_failure_response_into_error(response).await) + } + else { + response + .json() + .await + .map_err(VaultClientError::RequestFailed) + } + } + + pub async fn lookup_self(&self, auth_token: &str) -> Result { + let url = self.url.token(&self.path).lookup_self(); + + info!("Connecting to {}", url); + + let response = reqwest::Client::new() + .get(url) + .header(VAULT_TOKEN_HEADER, auth_token) + .send() + .await?; + + let status = response.status(); + + if status.is_server_error() || status.is_client_error() { + Err(read_failure_response_into_error(response).await) + } + else { + response + .json() + .await + .map_err(VaultClientError::RequestFailed) + } + } + +} + pub struct VaultAuthUserpassApi { url: VaultApiUrl, path: String @@ -291,7 +384,7 @@ pub struct VaultAuthUserpassApi { impl VaultAuthUserpassApi { - pub fn new(url: VaultApiUrl, path: &str) -> Self { + fn new(url: VaultApiUrl, path: &str) -> Self { Self { url, path: path.into() @@ -746,6 +839,25 @@ mod test_vault_api_url { assert_eq!("/v1/a/keys/b", url.transit("a", "b")); } + #[cfg(test)] + mod token { + use crate::client::VaultApiUrl; + + #[test] + fn create() { + let url = VaultApiUrl::new(""); + + assert_eq!("/v1/auth/token/create", url.token("token").create()); + } + + #[test] + fn lookup_self() { + let url = VaultApiUrl::new(""); + + assert_eq!("/v1/auth/foo/lookup-self", url.token("foo").lookup_self()); + } + } + #[test] fn userpass_login() { let url = VaultApiUrl::new(""); diff --git a/src/models.rs b/src/models.rs index d808bb9..d9e6d89 100644 --- a/src/models.rs +++ b/src/models.rs @@ -4,7 +4,7 @@ */ use std::borrow::Borrow; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use serde_derive::{Deserialize, Serialize}; use serde_json::Value; @@ -164,6 +164,83 @@ pub struct VaultPluginCatalogData { pub secret: Vec, } +/// https://developer.hashicorp.com/vault/api-docs/v1.11.x/auth/token +#[derive(Clone, Debug, Default, Serialize)] +pub struct VaultAuthTokenCreateRequest { + pub id: Option, + pub role_name: Option, + pub policies: Vec, + pub meta: HashMap, + pub no_parent: Option, + pub no_default_policy: Option, + pub renewable: Option, + // lease field omitted due to deprecation comment in docs + pub ttl: Option, + pub type_: Option, + pub explicit_max_ttl: Option, + pub display_name: Option, + pub num_uses: Option, + pub period: Option, + pub entity_alias: Option +} + +/// https://developer.hashicorp.com/vault/api-docs/v1.11.x/auth/token +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub struct VaultAuthTokenCreateResponse { + pub request_id: String, + pub lease_id: String, + pub renewable: bool, + pub lease_duration: u64, + pub data: Value, + pub wrap_info: Value, + pub warnings: Vec, + pub auth: VaultAuthTokenCreateResponseAuth, +} + +/// https://developer.hashicorp.com/vault/api-docs/v1.11.x/auth/token +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub struct VaultAuthTokenCreateResponseAuth { + pub client_token: String, + pub accessor: String, + pub policies: Vec, + pub token_policies: Vec, + pub metadata: HashMap, + pub lease_duration: u64, + pub renewable: bool, + pub entity_id: String, + pub token_type: String, + pub orphan: bool, + pub num_uses: u64, +} + +/// https://developer.hashicorp.com/vault/api-docs/v1.11.x/auth/token +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub struct VaultAuthTokenLookupSelfResponse { + pub data: VaultAuthTokenLookupSelfResponseData, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub struct VaultAuthTokenLookupSelfResponseData { + pub accessor: String, + pub creation_time: u64, + pub creation_ttl: u64, + pub display_name: String, + pub entity_id: String, + pub expire_time: String, + pub explicit_max_ttl: u64, + pub id: String, + // Included in documentation but not in response + //pub identity_policies: Vec, + pub issue_time: String, + pub meta: HashMap, + pub num_uses: u64, + pub orphan: bool, + pub path: String, + pub policies: Vec, + pub renewable: bool, + pub ttl: u64, +} + /// See https://developer.hashicorp.com/vault/api-docs/v1.11.x/auth/userpass #[derive(Clone, Debug, Default, Serialize)] pub struct VaultAuthUserpassCreateRequest { diff --git a/tests/test_vault_auth_tokens.rs b/tests/test_vault_auth_tokens.rs new file mode 100644 index 0000000..3ef935b --- /dev/null +++ b/tests/test_vault_auth_tokens.rs @@ -0,0 +1,140 @@ +#![cfg(not(windows))] +#![cfg(not(target_os = "macos"))] + +#[path = "../examples/example_utils/lib.rs"] +mod example_utils; + +use std::collections::HashMap; +use http::StatusCode; +use log::*; +use passivized_vault_client::client::{VaultApi, VaultApiUrl}; +use passivized_vault_client::errors::VaultClientError; +use passivized_vault_client::models::{VaultInitRequest, VaultUnsealRequest, VaultUnsealProgress, VaultAuthTokenCreateRequest}; + +#[tokio::test] +async fn test_create_and_read_tokens() { + use example_utils::container::VaultContainer; + + const FN: &str = "test_create_and_read_tokens"; + + passivized_test_support::logging::enable(); + + let vc = VaultContainer::new(FN) + .await + .unwrap(); + + let root_token = init_and_unseal(vc.url.clone()) + .await + .unwrap(); + + create_and_read_tokens(vc.url.clone(), &root_token) + .await + .unwrap(); + + vc.teardown() + .await + .unwrap(); +} + +async fn init_and_unseal(url: VaultApiUrl) -> Result { + let vault = VaultApi::new(url); + + let status = vault.get_status().await?; + + assert!(!status.initialized, "Vault not initialized"); + assert!(status.sealed, "Vault is sealed"); + + info!("Initializing Vault unseal keys"); + + let unseal_init_request = VaultInitRequest { + pgp_keys: None, + root_token_pgp_key: None, + secret_shares: 1, + secret_threshold: 1, + stored_shares: None, + recovery_shares: None, + recovery_threshold: None, + recovery_pgp_keys: None + }; + + let unseal_init_response = vault.initialize(&unseal_init_request).await?; + info!("Unseal init response:\n{:?}", unseal_init_response); + + let post_unseal_init_status = vault.get_status().await?; + info!("Status after unseal initialization:\n{:?}", post_unseal_init_status); + + info!("Unsealing"); + + for i in 0..unseal_init_request.secret_threshold { + let unseal_key = (&unseal_init_response + .keys_base64) + .get(i).unwrap(); + + let unseal_request = VaultUnsealRequest { + key: Some(unseal_key.into()), + reset: false, + migrate: false + }; + + let unseal_response = vault.unseal(&unseal_request).await?; + + info!("Unsealed {}", unseal_response.unseal_progress_string()) + } + + let post_unseal_status = vault.get_status().await?; + info!("Status after unseal requests:\n{:?}", post_unseal_status); + + assert!(!post_unseal_status.sealed, "Not sealed"); + + info!("Root token: {}", unseal_init_response.root_token); + + Ok(unseal_init_response.root_token) +} + +async fn create_and_read_tokens(url: VaultApiUrl, root_token: &str) -> Result<(), VaultClientError> { + let vault = VaultApi::new(url); + + let create_request = VaultAuthTokenCreateRequest { + meta: HashMap::from([("foo".into(), "bar".into())]), + display_name: Some("qux".into()), + num_uses: Some(7), + ttl: Some("3h".into()), + ..Default::default() + }; + + let created = vault.auth().tokens().create(root_token, &create_request) + .await? + .auth; + + assert_eq!(vec!["root".to_string()], created.policies); + assert_eq!(vec!["root".to_string()], created.token_policies); + assert_eq!(HashMap::from([("foo".into(), "bar".into())]), created.metadata); + assert_eq!(60 * 60 * 3, created.lease_duration); + assert_eq!(7, created.num_uses); + + let lookup = vault.auth().tokens().lookup_self(&created.client_token) + .await? + .data; + + assert_eq!("token-qux", lookup.display_name); + assert_eq!(vec!["root".to_string()], lookup.policies); + assert_eq!(HashMap::from([("foo".into(), "bar".into())]), lookup.meta); + assert_eq!(60 * 60 * 3, lookup.creation_ttl); + assert_eq!(7 - 1, lookup.num_uses); + assert!(lookup.creation_ttl <= 60 * 60 * 3); + assert!(lookup.creation_ttl >= 60 * 60 * 3 - 60); + + let response = vault.auth().tokens().lookup_self("garbledygook") + .await + .unwrap_err(); + + if let VaultClientError::FailureResponse(status, json) = response { + assert_eq!(StatusCode::FORBIDDEN, status); + assert!(json.contains("permission denied")); + } + else { + panic!("Unexpected response: {:?}", response); + } + + Ok(()) +} \ No newline at end of file