diff --git a/src/api.rs b/src/api.rs index 0f902c8..6377efd 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod consul; pub mod database; pub mod kv1; pub mod kv2; diff --git a/src/api/consul.rs b/src/api/consul.rs new file mode 100644 index 0000000..116da0f --- /dev/null +++ b/src/api/consul.rs @@ -0,0 +1,2 @@ +pub mod requests; +pub mod responses; diff --git a/src/api/consul/requests.rs b/src/api/consul/requests.rs new file mode 100644 index 0000000..409e2ea --- /dev/null +++ b/src/api/consul/requests.rs @@ -0,0 +1,139 @@ +use super::responses::{GenerateConsulCredsResponse, ListRolesResponse, ReadRoleResponse}; +use rustify_derive::Endpoint; + +/// ## Create/Update access Config +/// This endpoint creates or updates a consul secret engines access configuration. +/// +/// * Path: {self.mount}/config/access +/// * Method: POST +/// * Response: N/A +/// * Reference: https://www.vaultproject.io/api-docs/secret/consul#configure-access +#[derive(Builder, Debug, Default, Endpoint)] +#[endpoint(path = "{self.mount}/config/access", method = "POST", builder = "true")] +#[builder(setter(into, strip_option), default)] +pub struct SetAccessConfigRequest { + #[endpoint(skip)] + pub mount: String, + pub address: String, + pub schema: Option, + pub token: Option, + pub ca_cert: Option, + pub client_cert: Option, + pub client_key: Option, +} + +/// ## Create Role +/// This endpoint creates or updates a named role. +/// +/// * Path: {self.mount}/roles/{self.name} +/// * Method: POST +/// * Response: N/A +/// * Reference: https://www.vaultproject.io/api-docs/secret/consul#configure-access +#[derive(Builder, Debug, Default, Endpoint)] +#[endpoint( + path = "{self.mount}/roles/{self.name}", + method = "POST", + builder = "true" +)] +#[builder(setter(into, strip_option), default)] +pub struct SetRoleRequest { + #[endpoint(skip)] + pub mount: String, + pub name: String, + pub token_type: Option, // DEPRECATED since consul version 1.4 and removed in 1.11 + pub partition: Option, + pub node_identities: Option>, + pub consul_namespace: Option, + pub service_identities: Option>, + pub consul_roles: Option>, + pub consul_policies: Option>, + pub policy: Option, // DEPRECATED since consul version 1.4 and removed in 1.11 + pub policies: Option>, // DEPRECATED since consul version 1.4 and removed in 1.11 + pub local: Option, + pub max_ttl: Option, + pub ttl: Option, +} + +/// ## Read Role +/// This endpoint queries a named role. +/// +/// * Path: {self.mount}/roles/{self.name} +/// * Method: GET +/// * Response: [ReadRoleResponse] +/// * Reference: https://www.vaultproject.io/api-docs/secret/consul#read-role +#[derive(Builder, Debug, Default, Endpoint)] +#[endpoint( + path = "{self.mount}/roles/{self.name}", + response = "ReadRoleResponse", + builder = "true" +)] +#[builder(setter(into, strip_option), default)] +pub struct ReadRoleRequest { + #[endpoint(skip)] + pub mount: String, + #[endpoint(skip)] + pub name: String, +} + +/// ## List Roles +/// This endpoint returns a list of available roles. +/// +/// * Path: {self.mount}/roles +/// * Method: LIST +/// * Response: [ListRolesResponse] +/// * Reference: https://www.vaultproject.io/api-docs/secret/consul#list-roles +#[derive(Builder, Debug, Default, Endpoint)] +#[endpoint( + path = "{self.mount}/roles", + method = "LIST", + response = "ListRolesResponse", + builder = "true" +)] +#[builder(setter(into, strip_option), default)] +pub struct ListRolesRequest { + #[endpoint(skip)] + pub mount: String, +} + +/// ## Delete Role +/// This endpoint deletes a named role. +/// +/// * Path: {self.mount}/roles/{self.name} +/// * Method: DELETE +/// * Response: N/A +/// * Reference: https://www.vaultproject.io/api-docs/secret/consul#delete-role +#[derive(Builder, Debug, Default, Endpoint)] +#[endpoint( + path = "{self.mount}/roles/{self.name}", + method = "DELETE", + builder = "true" +)] +#[builder(setter(into, strip_option), default)] +pub struct DeleteRoleRequest { + #[endpoint(skip)] + pub mount: String, + #[endpoint(skip)] + pub name: String, +} + +/// ## Generate Consul Credentials +/// This endpoint creates credentials with the parameters defined in the given role. +/// +/// * Path: {self.mount}/creds/{self.name} +/// * Method: POST +/// * Response: [GenerateConsulCredsResponse] +/// * Reference: https://www.vaultproject.io/api-docs/secret/consul#generate-credential +#[derive(Builder, Debug, Default, Endpoint)] +#[endpoint( + path = "{self.mount}/creds/{self.name}", + method = "POST", + response = "GenerateConsulCredsResponse", + builder = "true" +)] +#[builder(setter(into, strip_option), default)] +pub struct GenerateConsulCredsRequest { + #[endpoint(skip)] + pub mount: String, + #[endpoint(skip)] + pub name: String, +} diff --git a/src/api/consul/responses.rs b/src/api/consul/responses.rs new file mode 100644 index 0000000..e07f93b --- /dev/null +++ b/src/api/consul/responses.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; + +/// Response from executing +/// [ReadRoleRequest][crate::api::consul::requests::ReadRoleRequest] +#[derive(Deserialize, Debug, Serialize)] +pub struct ReadRoleResponse { + pub token_type: Option, // DEPRECATED since consul version 1.4 and removed in 1.11 + pub partition: Option, + pub node_identities: Option>, + pub consul_namespace: Option, + pub service_identities: Option>, + pub consul_roles: Option>, + pub policy: Option, // DEPRECATED since consul version 1.4 and removed in 1.11 + pub policies: Option>, // DEPRECATED since consul version 1.4 and removed in 1.11 + pub consul_policies: Option>, + pub local: bool, + pub max_ttl: u64, + pub ttl: u64, +} + +/// Response from executing +/// [ListRolesRequest][crate::api::consul::requests::ListRolesRequest] +#[derive(Deserialize, Debug, Serialize)] +pub struct ListRolesResponse { + pub keys: Vec, +} + +/// Response from executing +/// [GenerateConsulCredsRequest][crate::api::consul::requests::GenerateConsulCredsRequest] +#[derive(Deserialize, Debug, Serialize)] +pub struct GenerateConsulCredsResponse { + pub token: String, +} diff --git a/src/consul.rs b/src/consul.rs new file mode 100644 index 0000000..6478d33 --- /dev/null +++ b/src/consul.rs @@ -0,0 +1,115 @@ +use crate::api; +use crate::api::consul::requests::GenerateConsulCredsRequest; +use crate::api::consul::responses::GenerateConsulCredsResponse; +use crate::client::Client; +use crate::error::ClientError; + +/// Generates Consul credentials for the given role +/// +/// See [GenerateConsulCredsRequest] +#[instrument(skip(client), err)] +pub async fn generate( + client: &impl Client, + mount: &str, + name: &str, +) -> Result { + let endpoint = GenerateConsulCredsRequest::builder() + .mount(mount) + .name(name) + .build() + .unwrap(); + api::exec_with_result(client, endpoint).await +} + +pub mod config { + use crate::api; + use crate::api::consul::requests::{SetAccessConfigRequest, SetAccessConfigRequestBuilder}; + use crate::client::Client; + use crate::error::ClientError; + + /// Creates or updates Consul access config + /// + /// See [SetAccessConfigRequest] + #[instrument(skip(client, opts), err)] + pub async fn set( + client: &impl Client, + mount: &str, + opts: Option<&mut SetAccessConfigRequestBuilder>, + ) -> Result<(), ClientError> { + let mut t = SetAccessConfigRequest::builder(); + let endpoint = opts.unwrap_or(&mut t).mount(mount).build().unwrap(); + api::exec_with_empty(client, endpoint).await + } +} + +pub mod role { + use crate::api; + use crate::api::consul::{ + requests::{ + DeleteRoleRequest, ListRolesRequest, ReadRoleRequest, SetRoleRequest, + SetRoleRequestBuilder, + }, + responses::{ListRolesResponse, ReadRoleResponse}, + }; + use crate::client::Client; + use crate::error::ClientError; + + /// Deletes a role + /// + /// See [DeleteRoleRequest] + #[instrument(skip(client), err)] + pub async fn delete(client: &impl Client, mount: &str, name: &str) -> Result<(), ClientError> { + let endpoint = DeleteRoleRequest::builder() + .mount(mount) + .name(name) + .build() + .unwrap(); + api::exec_with_empty(client, endpoint).await + } + + /// Lists all roles + /// + /// See [ListRolesRequest] + #[instrument(skip(client), err)] + pub async fn list(client: &impl Client, mount: &str) -> Result { + let endpoint = ListRolesRequest::builder().mount(mount).build().unwrap(); + api::exec_with_result(client, endpoint).await + } + + /// Reads a role + /// + /// See [ReadRoleRequest] + #[instrument(skip(client), err)] + pub async fn read( + client: &impl Client, + mount: &str, + name: &str, + ) -> Result { + let endpoint = ReadRoleRequest::builder() + .mount(mount) + .name(name) + .build() + .unwrap(); + api::exec_with_result(client, endpoint).await + } + + /// Creates or updates a role + /// + /// See [SetRoleRequest] + #[instrument(skip(client, opts), err)] + pub async fn set( + client: &impl Client, + mount: &str, + name: &str, + opts: Option<&mut SetRoleRequestBuilder>, + ) -> Result<(), ClientError> { + let mut t = SetRoleRequest::builder(); + let endpoint = opts + .unwrap_or(&mut t) + .mount(mount) + .name(name) + .build() + .unwrap(); + api::exec_with_empty(client, endpoint).await + } +} diff --git a/src/lib.rs b/src/lib.rs index da82f84..44d1ec1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ //! * [Token](https://www.vaultproject.io/docs/auth/token) //! * [Userpass](https://www.vaultproject.io/docs/auth/userpass) //! * Secrets +//! * [Consul](https://www.vaultproject.io/api-docs/secret/consul) //! * [Databases](https://www.vaultproject.io/api-docs/secret/databases) //! * [KV v2](https://www.vaultproject.io/docs/secrets/kv/kv-v2) //! * [PKI](https://www.vaultproject.io/docs/secrets/pki) @@ -199,6 +200,7 @@ extern crate tracing; pub mod api; pub mod auth; pub mod client; +pub mod consul; pub mod database; pub mod error; pub mod kv1; diff --git a/tests/common.rs b/tests/common.rs index 85852bb..06c0342 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,7 +1,9 @@ use async_trait::async_trait; pub use dockertest_server::servers::cloud::localstack::{LocalStackServer, LocalStackServerConfig}; pub use dockertest_server::servers::database::postgres::{PostgresServer, PostgresServerConfig}; -pub use dockertest_server::servers::hashi::{VaultServer, VaultServerConfig}; +pub use dockertest_server::servers::hashi::{ + ConsulServer, ConsulServerConfig, VaultServer, VaultServerConfig, +}; use dockertest_server::servers::webserver::nginx::{ ManagedContent, NginxServerConfig, WebserverContent, }; @@ -151,6 +153,20 @@ pub fn new_test() -> Test { test } +// Sets up a new consul test. +#[allow(dead_code)] +pub fn new_consul_test() -> Test { + let mut test = new_test(); + let consul_config = ConsulServerConfig::builder() + .port(8500) + .version("1.15".to_string()) + .token("test".to_string()) + .build() + .unwrap(); + test.register(consul_config); + test +} + // Sets up a new database test. #[allow(dead_code)] pub fn new_db_test() -> Test { diff --git a/tests/consul.rs b/tests/consul.rs new file mode 100644 index 0000000..eafc4d1 --- /dev/null +++ b/tests/consul.rs @@ -0,0 +1,98 @@ +#[macro_use] +extern crate tracing; + +mod common; + +use common::{ConsulServer, VaultServer, VaultServerHelper}; +use test_log::test; +use vaultrs::api::consul::requests::SetAccessConfigRequest; +use vaultrs::client::Client; +use vaultrs::error::ClientError; + +#[test] +fn test() { + let test = common::new_consul_test(); + test.run(|instance| async move { + let consul_server: ConsulServer = instance.server(); + let vault_server: VaultServer = instance.server(); + let client = vault_server.client(); + let endpoint = setup(&consul_server, &vault_server, &client).await.unwrap(); + + // Test roles + crate::role::test_set(&client, &endpoint).await; + crate::role::test_read(&client, &endpoint).await; + crate::role::test_list(&client, &endpoint).await; + crate::role::test_delete(&client, &endpoint).await; + }); +} + +mod role { + use super::{Client, ConsulEndpoint}; + use vaultrs::{api::consul::requests::SetRoleRequest, consul::role}; + + pub async fn test_delete(client: &impl Client, endpoint: &ConsulEndpoint) { + let res = role::delete(client, endpoint.path.as_str(), endpoint.role.as_str()).await; + assert!(res.is_ok()); + } + + pub async fn test_list(client: &impl Client, endpoint: &ConsulEndpoint) { + let res = role::list(client, endpoint.path.as_str()).await; + assert!(res.is_ok()); + assert!(!res.unwrap().keys.is_empty()); + } + + pub async fn test_read(client: &impl Client, endpoint: &ConsulEndpoint) { + let res = role::read(client, endpoint.path.as_str(), endpoint.role.as_str()).await; + assert!(res.is_ok()); + } + + pub async fn test_set(client: &impl Client, endpoint: &ConsulEndpoint) { + let policies = vec!["global-management".to_string()]; + let res = role::set( + client, + endpoint.path.as_str(), + endpoint.role.as_str(), + Some(SetRoleRequest::builder().policies(policies)), + ) + .await; + assert!(res.is_ok()); + } +} + +#[derive(Debug)] +pub struct ConsulEndpoint { + pub connection: String, + pub path: String, + pub role: String, +} + +async fn setup( + consul_server: &ConsulServer, + vault_server: &VaultServer, + client: &impl Client, +) -> Result { + debug!("setting up consul secret engine"); + + let connection = "consul"; + let path = "consul_test"; + let role = "test"; + + // Mount the database secret engine + vault_server.mount_secret(client, path, "consul").await?; + vaultrs::consul::config::set( + client, + path, + Some( + SetAccessConfigRequest::builder() + .address(consul_server.internal_address()) + .token("test".to_string()), + ), + ) + .await?; + + Ok(ConsulEndpoint { + connection: connection.to_string(), + path: path.to_string(), + role: role.to_string(), + }) +}