Skip to content

Commit

Permalink
Merge pull request #79 from stormshield-gt/add_sys_init_endpoint
Browse files Browse the repository at this point in the history
add sys init endpoint
  • Loading branch information
Haennetz authored Mar 20, 2024
2 parents f317bfa + 42c1452 commit b1a5ebc
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 6 deletions.
43 changes: 42 additions & 1 deletion src/api/sys/requests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::responses::{
AuthResponse, ListPoliciesResponse, MountResponse, ReadHealthResponse, ReadPolicyResponse,
UnsealResponse, WrappingLookupResponse,
StartInitializationResponse, UnsealResponse, WrappingLookupResponse,
};
use rustify_derive::Endpoint;
use serde::Serialize;
Expand Down Expand Up @@ -160,6 +160,47 @@ pub struct WrappingLookupRequest {
#[builder(setter(into), default)]
pub struct ReadHealthRequest {}

/// ## Start Initialization
///
/// This endpoint initializes a new Vault. The Vault must not have been previously initialized.
/// The recovery options, as well as the stored shares option, are only available when using Auto Unseal.
///
/// * Path: /sys/init
/// * Method: POST
/// * Response: [StartInitializationResponse]
/// * Reference: https://developer.hashicorp.com/vault/api-docs/system/init#start-initialization
#[derive(Builder, Default, Endpoint)]
#[endpoint(
path = "/sys/init",
method = "POST",
response = "StartInitializationResponse",
builder = "true"
)]
#[builder(setter(into), default)]
pub struct StartInitializationRequest {
/// Specifies an array of PGP public keys used to encrypt the output unseal keys. Ordering is preserved.
/// The keys must be base64-encoded from their original binary representation. The size of this array must be the same as secret_shares.
pgp_keys: Option<Vec<String>>,
/// Specifies a PGP public key used to encrypt the initial root token. The key must be base64-encoded from its original binary representation.
root_token_pgp_key: Option<String>,
/// Specifies the number of shares to split the root key into.
secret_shares: u64,
/// Specifies the number of shares required to reconstruct the root key. This must be less than or equal secret_shares.
secret_threshold: u64,

/// Additionally, the following options are only supported using Auto Unseal:
/// Specifies the number of shares that should be encrypted by the HSM and stored for auto-unsealing. Currently must be the same as secret_shares.
stored_shares: Option<u64>,
/// Specifies the number of shares to split the recovery key into. This is only available when using Auto Unseal.
recovery_shares: Option<u64>,
/// Specifies the number of shares required to reconstruct the recovery key. This must be less than or equal to recovery_shares.
/// This is only available when using Auto Unseal.
recovery_threshold: Option<u64>,
/// Specifies an array of PGP public keys used to encrypt the output recovery keys. Ordering is preserved.
/// The keys must be base64-encoded from their original binary representation. The size of this array must be the same as recovery_shares. This is only available when using Auto Unseal.
recovery_pgp_keys: Option<Vec<String>>,
}

/// ## Seal
/// This endpoint seals the Vault.
///
Expand Down
9 changes: 9 additions & 0 deletions src/api/sys/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ pub struct ReadHealthResponse {
pub version: String,
}

/// Response from executing
/// [StartInitializationRequest][crate::api::sys::requests::StartInitializationRequest]
#[derive(Deserialize, Debug, Serialize)]
pub struct StartInitializationResponse {
pub keys: Vec<String>,
pub keys_base64: Vec<String>,
pub root_token: String,
}

/// Response from executing
/// [UnsealRequest][crate::api::sys::requests::UnsealRequest]
#[derive(Deserialize, Debug, Serialize)]
Expand Down
27 changes: 25 additions & 2 deletions src/sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ use crate::{
api::{
self,
sys::{
requests::{ReadHealthRequest, SealRequest, UnsealRequest},
responses::{ReadHealthResponse, UnsealResponse},
requests::{
ReadHealthRequest, SealRequest, StartInitializationRequest,
StartInitializationRequestBuilder, UnsealRequest,
},
responses::{ReadHealthResponse, StartInitializationResponse, UnsealResponse},
},
},
client::Client,
Expand Down Expand Up @@ -31,6 +34,26 @@ pub async fn health(client: &impl Client) -> Result<ReadHealthResponse, ClientEr
api::exec_with_no_result(client, endpoint).await
}

/// Initialize a new Vault. The Vault must not have been previously initialized.
///
/// See [StartInitializationRequest]
#[instrument(skip(client, opts), err)]
pub async fn start_initialization(
client: &impl Client,
secret_shares: u64,
secret_threshold: u64,
opts: Option<&mut StartInitializationRequestBuilder>,
) -> Result<StartInitializationResponse, ClientError> {
let mut t = StartInitializationRequest::builder();
let endpoint = opts
.unwrap_or(&mut t)
.secret_shares(secret_shares)
.secret_threshold(secret_threshold)
.build()
.unwrap();
api::exec_with_no_result(client, endpoint).await
}

/// Seals the Vault server.
///
/// See [SealRequest]
Expand Down
76 changes: 73 additions & 3 deletions tests/sys.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
mod common;
mod vault_prod_container;

use common::{VaultServer, VaultServerHelper};
use std::collections::HashMap;

use common::{VaultServer, VaultServerHelper, PORT, VERSION};
use dockertest_server::Test;
use test_log::test;
use vaultrs::{
api::{sys::requests::ListMountsRequest, ResponseWrapper},
client::Client,
sys::{self},
client::{Client, VaultClient, VaultClientSettingsBuilder},
error::ClientError,
sys,
};

#[test]
Expand All @@ -21,6 +26,9 @@ fn test() {
// Test health
test_health(&client).await;

// Test initialization
test_start_initialization_failure(&client).await;

// Test status
test_status(&client).await;

Expand All @@ -43,6 +51,22 @@ fn test() {
});
}

#[test]
fn sys_init() {
let test = new_prod_test();
test.run(|instance| async move {
let server: vault_prod_container::VaultServer = instance.server();
let client = VaultClient::new(
VaultClientSettingsBuilder::default()
.address(server.external_url())
.build()
.unwrap(),
)
.unwrap();
test_start_initialization(&client).await;
});
}

async fn test_wrap(client: &impl Client) {
let endpoint = ListMountsRequest::builder().build().unwrap();
let wrap_resp = endpoint.wrap(client).await;
Expand All @@ -64,6 +88,21 @@ async fn test_health(client: &impl Client) {
assert!(resp.is_ok());
}

async fn test_start_initialization_failure(client: &impl Client) {
let resp = sys::start_initialization(client, 1, 1, None)
.await
.unwrap_err();
let ClientError::APIError { code, .. } = resp else {
panic!("must return an error because already initialized")
};
assert_eq!(code, 400);
}

async fn test_start_initialization(client: &impl Client) {
let resp = sys::start_initialization(client, 1, 1, None).await.unwrap();
assert_eq!(resp.keys.len(), 1);
}

async fn test_seal(client: &impl Client) {
let resp = sys::seal(client).await;
assert!(resp.is_ok());
Expand Down Expand Up @@ -134,3 +173,34 @@ mod policy {
assert!(resp.is_ok());
}
}

// Sets up a new test using the vault production server.
pub fn new_prod_test() -> Test {
let mut test = Test::default();
let vault_config = serde_json::json!({
"listener": [
{
"tcp": {
"address": "0.0.0.0:8300",
"tls_disable": "true"
}
}
],
"storage": [
{
"inmem": {}
}
],
"disable_mlock": true

});
let env = HashMap::from([("VAULT_LOCAL_CONFIG".to_string(), vault_config.to_string())]);
let config = vault_prod_container::VaultServerConfig::builder()
.port(PORT)
.version(VERSION.into())
.env(env)
.build()
.unwrap();
test.register(config);
test
}
115 changes: 115 additions & 0 deletions tests/vault_prod_container.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// This file is copied from https://github.com/jmgilman/dockertest-server/blob/master/src/servers/hashi/vault.rs
// because of the LOG_MSG condition is only applicable to vault dev server.

use derive_builder::Builder;
use dockertest::{waitfor, Source};
use dockertest_server::{Config, ContainerConfig, Server};
use std::collections::HashMap;

const IMAGE: &str = "vault";
const PORT: u32 = 8300;
const LOG_MSG: &str = "Vault server started!";
const SOURCE: Source = Source::DockerHub;

/// Configuration for creating a Hashicorp Vault server.
///
/// A token with root permissions will automatically be generated using the
/// `token` field. If it's omitted the token will automatically be generated.
///
/// By default the Vault server listens on port 8200 for HTTP requests. This
/// is exposed on the container by default, but the exposed port can be
/// controlled by setting the `port` field.
///
/// See the [Dockerhub](https://hub.docker.com/_/vault) page for more
/// information on the arguments and environment variables that can be used to
/// configure the server.
#[derive(Clone, Default, Builder)]
#[builder(default)]
pub struct VaultServerConfig {
#[builder(default = "vec![String::from(\"server\")]")]
pub args: Vec<String>,
#[builder(default = "HashMap::new()")]
pub env: HashMap<String, String>,
#[builder(default = "dockertest_server::server::new_handle(IMAGE)")]
pub handle: String,
#[builder(default = "8200")]
pub port: u32,
#[builder(default = "15")]
pub timeout: u16,
#[builder(default = "String::from(\"latest\")")]
pub version: String,
}

impl VaultServerConfig {
pub fn builder() -> VaultServerConfigBuilder {
VaultServerConfigBuilder::default()
}
}

impl Config for VaultServerConfig {
fn into_composition(self) -> dockertest::Composition {
let ports = vec![(PORT, self.port)];

let timeout = self.timeout;
let wait = Box::new(waitfor::MessageWait {
message: LOG_MSG.into(),
source: waitfor::MessageSource::Stdout,
timeout,
});

ContainerConfig {
args: self.args,
env: self.env,
handle: self.handle,
name: IMAGE.into(),
source: SOURCE,
version: self.version,
ports: Some(ports),
wait: Some(wait),
bind_mounts: HashMap::new(),
}
.into()
}

fn handle(&self) -> &str {
self.handle.as_str()
}
}

/// A running instance of a Vault server.
///
/// The server URL which is accessible from the local host can be found in `local_address`.
/// Other running containers which need access to this server should use the
/// `address` field instead.
pub struct VaultServer {
pub external_port: u32,
pub internal_port: u32,
pub ip: String,
}

impl VaultServer {
fn format_address(&self, host: &str, port: u32) -> String {
format!("{}:{}", host, port)
}

fn format_url(&self, host: &str, port: u32) -> String {
format!("http://{}", self.format_address(host, port))
}

/// The external HTTP address
pub fn external_url(&self) -> String {
self.format_url("localhost", self.external_port)
}
}

impl Server for VaultServer {
type Config = VaultServerConfig;

fn new(config: &Self::Config, container: &dockertest::RunningContainer) -> Self {
VaultServer {
external_port: config.port,
internal_port: PORT,
ip: container.ip().to_string(),
}
}
}

0 comments on commit b1a5ebc

Please sign in to comment.