Skip to content

Commit

Permalink
Basic token auth (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
iamjpotts authored Nov 15, 2022
1 parent bd8dfa8 commit 947e5b5
Show file tree
Hide file tree
Showing 3 changed files with 331 additions and 2 deletions.
114 changes: 113 additions & 1 deletion src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand All @@ -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
Expand Down Expand Up @@ -279,19 +303,88 @@ 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<VaultAuthTokenCreateResponse, VaultClientError> {
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<VaultAuthTokenLookupSelfResponse, VaultClientError> {
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
}

impl VaultAuthUserpassApi {

pub fn new(url: VaultApiUrl, path: &str) -> Self {
fn new(url: VaultApiUrl, path: &str) -> Self {
Self {
url,
path: path.into()
Expand Down Expand Up @@ -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("");
Expand Down
79 changes: 78 additions & 1 deletion src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -164,6 +164,83 @@ pub struct VaultPluginCatalogData {
pub secret: Vec<String>,
}

/// https://developer.hashicorp.com/vault/api-docs/v1.11.x/auth/token
#[derive(Clone, Debug, Default, Serialize)]
pub struct VaultAuthTokenCreateRequest {
pub id: Option<String>,
pub role_name: Option<String>,
pub policies: Vec<String>,
pub meta: HashMap<String, String>,
pub no_parent: Option<bool>,
pub no_default_policy: Option<bool>,
pub renewable: Option<bool>,
// lease field omitted due to deprecation comment in docs
pub ttl: Option<String>,
pub type_: Option<String>,
pub explicit_max_ttl: Option<String>,
pub display_name: Option<String>,
pub num_uses: Option<u64>,
pub period: Option<String>,
pub entity_alias: Option<String>
}

/// 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<String>,
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<String>,
pub token_policies: Vec<String>,
pub metadata: HashMap<String, String>,
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<String>,
pub issue_time: String,
pub meta: HashMap<String, String>,
pub num_uses: u64,
pub orphan: bool,
pub path: String,
pub policies: Vec<String>,
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 {
Expand Down
140 changes: 140 additions & 0 deletions tests/test_vault_auth_tokens.rs
Original file line number Diff line number Diff line change
@@ -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<String, VaultClientError> {
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(())
}

0 comments on commit 947e5b5

Please sign in to comment.