Skip to content

Commit 13bc862

Browse files
committed
Add PKI config dir logic and restructure DSH module
1 parent 76928f6 commit 13bc862

File tree

10 files changed

+1061
-794
lines changed

10 files changed

+1061
-794
lines changed

dsh_sdk/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ hyper = { version = "1.3", features = ["server", "http1"], optional = true }
2121
hyper-util = { version = "0.1", features = ["tokio"], optional = true }
2222
lazy_static = { version = "1.4", optional = true }
2323
log = "0.4"
24+
pem = "3"
2425
prometheus = { version = "0.13", features = ["process"], optional = true }
2526
rcgen = { version = "0.13", optional = true }
2627
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "blocking"], optional = true }

dsh_sdk/src/dsh/bootstrap.rs

Lines changed: 72 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -5,101 +5,82 @@
55
//! used in the get_signed_certificates_json.sh script.
66
//!
77
//! ## Note
8-
//!
98
//! This module is not intended to be used directly, but through the `Properties` struct. It will
10-
//! always be used when getting a `Properties` struct vja dsh::Properties::get().
11-
//!
12-
//! If this module returns an error, it defaults to the local_datastreams.json file, so it can be used
13-
//! in a local environment. (when feature `local` is enabled)
9+
//! always be used when getting a `Properties` struct via dsh::Properties::get().
1410
//!
15-
//! ## Example
16-
//! ```
17-
//! use dsh_sdk::Properties;
18-
//!
19-
//! let dsh_properties = Properties::get();
20-
//! ```
11+
//! If this module returns an error, it defaults to the local_datastreams.json file or the
12+
//! default properties. This means it can be used in you local development environment without
13+
//! the need to connect to DSH.
2114
22-
use log::{debug, info, warn};
15+
use log::{info, warn};
2316
use reqwest::blocking::Client;
2417

25-
use std::env;
26-
2718
use crate::error::DshError;
2819

29-
use super::{certificates::Cert, datastream::Datastream, Properties};
20+
use super::{
21+
certificates::Cert, datastream::Datastream, utils, VAR_DSH_CA_CERTIFICATE,
22+
VAR_DSH_SECRET_TOKEN, VAR_DSH_SECRET_TOKEN_PATH, VAR_KAFKA_CONFIG_HOST,
23+
};
3024

31-
impl Properties {
32-
/// Connect to DSH and retrieve the certificates and datastreams.json to create the properties struct
33-
pub(crate) fn new_dsh() -> Result<Self, DshError> {
34-
let dsh_config = DshConfig::new()?;
35-
let client = Properties::reqwest_client(dsh_config.dsh_ca_certificate.as_bytes())?;
36-
let dn = DshCall::Dn(&dsh_config).perform_call(&client)?;
37-
let dn = Dn::parse_string(&dn)?;
38-
let certificates = Cert::new(dn, &dsh_config, &client)?;
39-
let client_with_cert = certificates.reqwest_blocking_client_config()?.build()?;
40-
let datastreams_string =
41-
DshCall::Datastream(&dsh_config).perform_call(&client_with_cert)?;
42-
let datastream: Datastream = serde_json::from_str(&datastreams_string)?;
43-
Ok(Self {
44-
client_id: dsh_config.task_id.to_string(),
45-
tenant_name: dsh_config.tenant_name.to_string(),
46-
task_id: dsh_config.task_id.to_string(),
47-
datastream,
48-
certificates: Some(certificates),
49-
})
50-
}
25+
/// Connect to DSH and retrieve the certificates and datastreams.json to create the properties struct
26+
pub(crate) fn bootstrap(tenant_name: &str, task_id: &str) -> Result<(Cert, Datastream), DshError> {
27+
let dsh_config = DshConfig::new(tenant_name, task_id)?;
28+
let client = reqwest_ca_client(dsh_config.dsh_ca_certificate.as_bytes())?;
29+
let dn = DshCall::Dn(&dsh_config).perform_call(&client)?;
30+
let dn = Dn::parse_string(&dn)?;
31+
let certificates = Cert::bootstrap(dn, &dsh_config, &client)?;
32+
let client_with_cert = certificates.reqwest_blocking_client_config()?.build()?;
33+
let datastreams_string = DshCall::Datastream(&dsh_config).perform_call(&client_with_cert)?;
34+
let datastream: Datastream = serde_json::from_str(&datastreams_string)?;
35+
Ok((certificates, datastream))
36+
}
5137

52-
/// Build a request client with the DSH CA certificate.
53-
fn reqwest_client(dsh_ca_certificate: &[u8]) -> Result<Client, reqwest::Error> {
54-
let reqwest_cert = reqwest::tls::Certificate::from_pem(dsh_ca_certificate)?;
55-
let client = Client::builder()
56-
.add_root_certificate(reqwest_cert)
57-
.build()?;
58-
Ok(client)
59-
}
38+
/// Build a request client with the DSH CA certificate.
39+
fn reqwest_ca_client(dsh_ca_certificate: &[u8]) -> Result<Client, reqwest::Error> {
40+
let reqwest_cert = reqwest::tls::Certificate::from_pem(dsh_ca_certificate)?;
41+
let client = Client::builder()
42+
.add_root_certificate(reqwest_cert)
43+
.build()?;
44+
Ok(client)
6045
}
6146

6247
#[derive(Debug)]
63-
pub(crate) struct DshConfig {
48+
pub(crate) struct DshConfig<'a> {
6449
config_host: String,
65-
tenant_name: String,
66-
task_id: String,
50+
tenant_name: &'a str,
51+
task_id: &'a str,
6752
dsh_secret_token: String,
6853
dsh_ca_certificate: String,
6954
}
7055

7156
/// Helper struct to store the config needed for bootstrapping to DSH
72-
impl DshConfig {
73-
fn new() -> Result<Self, DshError> {
74-
let config_host = Self::get_env_var("KAFKA_CONFIG_HOST")
57+
impl<'a> DshConfig<'a> {
58+
fn new(tenant_name: &'a str, task_id: &'a str) -> Result<Self, DshError> {
59+
let config_host = utils::get_env_var(VAR_KAFKA_CONFIG_HOST)
7560
.map(|host| format!("https://{}", host))
7661
.unwrap_or_else(|_| {
77-
let default = "https://pikachu.dsh.marathon.mesos:4443".to_string();
62+
let default = "https://pikachu.dsh.marathon.mesos:4443";
7863
warn!(
79-
"KAFKA_CONFIG_HOST is not set, using default value {}",
80-
default
64+
"{} is not set, using default value {}",
65+
VAR_KAFKA_CONFIG_HOST, default
8166
);
82-
default
67+
default.to_string()
8368
});
84-
85-
let task_id = Self::get_env_var("MESOS_TASK_ID")?;
86-
let app_id = Self::get_env_var("MARATHON_APP_ID")?;
87-
let dsh_secret_token = match Self::get_env_var("DSH_SECRET_TOKEN") {
69+
let dsh_secret_token = match utils::get_env_var(VAR_DSH_SECRET_TOKEN) {
8870
Ok(token) => token,
8971
Err(_) => {
9072
// if DSH_SECRET_TOKEN is not set, try to read it from a file (for system space applications)
9173
info!("trying to read DSH_SECRET_TOKEN from file");
92-
let secret_token_path = Self::get_env_var("DSH_SECRET_TOKEN_PATH")?;
74+
let secret_token_path = utils::get_env_var(VAR_DSH_SECRET_TOKEN_PATH)?;
9375
let path = std::path::PathBuf::from(secret_token_path);
9476
std::fs::read_to_string(path)?
9577
}
9678
};
97-
let dsh_ca_certificate = Self::get_env_var("DSH_CA_CERTIFICATE")?;
98-
let tenant_name = DshConfig::tenant_name(&app_id);
79+
let dsh_ca_certificate = utils::get_env_var(VAR_DSH_CA_CERTIFICATE)?;
9980
Ok(DshConfig {
10081
config_host,
10182
task_id,
102-
tenant_name: tenant_name.to_string(),
83+
tenant_name,
10384
dsh_secret_token,
10485
dsh_ca_certificate,
10586
})
@@ -108,41 +89,18 @@ impl DshConfig {
10889
pub(crate) fn dsh_ca_certificate(&self) -> &str {
10990
&self.dsh_ca_certificate
11091
}
111-
112-
/// Derive the tenant name from the app id.
113-
fn tenant_name(app_id: &str) -> &str {
114-
let tenant_name = app_id.split('/').nth(1);
115-
match tenant_name {
116-
Some(tenant_name) => tenant_name,
117-
None => {
118-
warn!(
119-
"MARATHON_APP_ID is not as expected, missing expected slashes, using \"{}\" as tenant name",
120-
app_id
121-
);
122-
app_id
123-
}
124-
}
125-
}
126-
127-
fn get_env_var(var_name: &str) -> Result<String, DshError> {
128-
debug!("Reading {} from environment variable", var_name);
129-
match env::var(var_name) {
130-
Ok(value) => Ok(value),
131-
Err(e) => {
132-
warn!("{} is not set", var_name);
133-
Err(e.into())
134-
}
135-
}
136-
}
13792
}
13893

13994
pub(crate) enum DshCall<'a> {
14095
/// Call to retreive distinguished name.
141-
Dn(&'a DshConfig),
96+
Dn(&'a DshConfig<'a>),
14297
/// Call to retreive datastreams.json.
143-
Datastream(&'a DshConfig),
98+
Datastream(&'a DshConfig<'a>),
14499
/// Call to post the certificate signing request.
145-
CertificateSignRequest { config: &'a DshConfig, csr: &'a str },
100+
CertificateSignRequest {
101+
config: &'a DshConfig<'a>,
102+
csr: &'a str,
103+
},
146104
}
147105

148106
impl DshCall<'_> {
@@ -248,35 +206,17 @@ impl Dn {
248206

249207
#[cfg(test)]
250208
mod tests {
251-
use std::str::from_utf8;
252-
253209
use super::*;
254-
255-
#[test]
256-
fn test_dsh_config_tenant_name() {
257-
let app_id = "/greenbox-dev/app-name";
258-
let result = DshConfig::tenant_name(app_id);
259-
assert_eq!(
260-
result,
261-
"greenbox-dev".to_string(),
262-
"{} is not parsed correctly",
263-
app_id
264-
);
265-
let app_id = "greenbox-dev";
266-
let result = DshConfig::tenant_name(app_id);
267-
assert_eq!(
268-
result, app_id,
269-
"{} is not parsed correctly, should be the same",
270-
app_id
271-
);
272-
}
210+
use serial_test::serial;
211+
use std::env;
212+
use std::str::from_utf8;
273213

274214
#[test]
275215
fn test_dsh_call_request_builder() {
276216
let dsh_config = DshConfig {
277217
config_host: "https://test_host".to_string(),
278-
tenant_name: "test_tenant_name".to_string(),
279-
task_id: "test_task_id".to_string(),
218+
tenant_name: "test_tenant_name",
219+
task_id: "test_task_id",
280220
dsh_secret_token: "test_token".to_string(),
281221
dsh_ca_certificate: "test_ca_certificate".to_string(),
282222
};
@@ -324,8 +264,8 @@ mod tests {
324264
// create a DshConfig struct
325265
let dsh_config = DshConfig {
326266
config_host: dsh.url(),
327-
tenant_name: "tenant".to_string(),
328-
task_id: "test_task_id".to_string(),
267+
tenant_name: "tenant",
268+
task_id: "test_task_id",
329269
dsh_secret_token: "test_token".to_string(),
330270
dsh_ca_certificate: "test_ca_certificate".to_string(),
331271
};
@@ -344,41 +284,39 @@ mod tests {
344284
}
345285

346286
#[test]
347-
fn test_get_env_var() {
348-
env::set_var("TEST_ENV_VAR", "test_value");
349-
let result = DshConfig::get_env_var("TEST_ENV_VAR").unwrap();
350-
assert_eq!(result, "test_value");
351-
}
352-
353-
#[test]
287+
#[serial(env_depencency)]
354288
fn test_dsh_config_new() {
355289
// normal situation where DSH variables are set
356-
env::set_var("KAFKA_CONFIG_HOST", "test_host");
357-
env::set_var("MESOS_TASK_ID", "test_task_id");
358-
env::set_var("MARATHON_APP_ID", "/test_tenant/test_app");
359-
env::set_var("DSH_SECRET_TOKEN", "test_token");
360-
env::set_var("DSH_CA_CERTIFICATE", "test_ca_certificate");
361-
let dsh_config = DshConfig::new().unwrap();
290+
env::set_var(VAR_KAFKA_CONFIG_HOST, "test_host");
291+
env::set_var(VAR_DSH_SECRET_TOKEN, "test_token");
292+
env::set_var(VAR_DSH_CA_CERTIFICATE, "test_ca_certificate");
293+
let tenant_name = "test_tenant";
294+
let task_id = "test_task_id";
295+
let dsh_config = DshConfig::new(tenant_name, task_id).unwrap();
362296
assert_eq!(dsh_config.config_host, "https://test_host");
363297
assert_eq!(dsh_config.task_id, "test_task_id");
364298
assert_eq!(dsh_config.tenant_name, "test_tenant");
365299
assert_eq!(dsh_config.dsh_secret_token, "test_token");
366300
assert_eq!(dsh_config.dsh_ca_certificate, "test_ca_certificate");
367301
// DSH_SECRET_TOKEN is not set, but DSH_SECRET_TOKEN_PATH is set
368-
env::remove_var("DSH_SECRET_TOKEN");
302+
env::remove_var(VAR_DSH_SECRET_TOKEN);
369303
let test_token_dir = "test_files";
370304
std::fs::create_dir_all(test_token_dir).unwrap();
371305
let test_token_dir = format!("{}/test_token", test_token_dir);
372306
let _ = std::fs::remove_file(&test_token_dir);
373-
env::set_var("DSH_SECRET_TOKEN_PATH", &test_token_dir);
374-
let result = DshConfig::new();
307+
env::set_var(VAR_DSH_SECRET_TOKEN_PATH, &test_token_dir);
308+
let result = DshConfig::new(tenant_name, task_id);
375309
assert!(result.is_err());
376310
std::fs::write(test_token_dir.as_str(), "test_token_from_file").unwrap();
377-
let dsh_config = DshConfig::new().unwrap();
311+
let dsh_config = DshConfig::new(tenant_name, task_id).unwrap();
378312
assert_eq!(dsh_config.dsh_secret_token, "test_token_from_file");
379313
// fail if DSH_CA_CERTIFICATE is not set
380-
env::remove_var("DSH_CA_CERTIFICATE");
381-
let result = DshConfig::new();
314+
env::remove_var(VAR_DSH_CA_CERTIFICATE);
315+
let result = DshConfig::new(tenant_name, task_id);
382316
assert!(result.is_err());
317+
env::remove_var(VAR_KAFKA_CONFIG_HOST);
318+
env::remove_var(VAR_DSH_SECRET_TOKEN);
319+
env::remove_var(VAR_DSH_CA_CERTIFICATE);
320+
env::remove_var(VAR_DSH_SECRET_TOKEN_PATH);
383321
}
384322
}

dsh_sdk/src/dsh/certificates.rs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,41 @@ use rcgen::{CertificateParams, CertificateSigningRequest, DnType, KeyPair};
5050
#[derive(Debug, Clone)]
5151
pub struct Cert {
5252
dsh_ca_certificate_pem: String,
53-
dsh_kafka_certificate_pem: String,
53+
dsh_client_certificate_pem: String,
5454
key_pair: Arc<KeyPair>,
5555
}
5656

5757
impl Cert {
58-
/// Create a new certificate struct.
59-
pub(crate) fn new(dn: Dn, dsh_config: &DshConfig, client: &Client) -> Result<Self, DshError> {
58+
/// Create new `Cert` struct
59+
pub(crate) fn new(
60+
dsh_ca_certificate_pem: String,
61+
dsh_client_certificate_pem: String,
62+
key_pair: KeyPair,
63+
) -> Cert {
64+
Self {
65+
dsh_ca_certificate_pem,
66+
dsh_client_certificate_pem,
67+
key_pair: Arc::new(key_pair),
68+
}
69+
}
70+
///
71+
pub(crate) fn bootstrap(
72+
dn: Dn,
73+
dsh_config: &DshConfig,
74+
client: &Client,
75+
) -> Result<Self, DshError> {
6076
let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P384_SHA384)?;
6177
let csr = Self::generate_csr(&key_pair, dn)?;
62-
let dsh_kafka_certificate_pem = DshCall::CertificateSignRequest {
78+
let client_certificate = DshCall::CertificateSignRequest {
6379
config: dsh_config,
6480
csr: &csr.pem()?,
6581
}
6682
.perform_call(client)?;
67-
Ok(Self {
68-
dsh_ca_certificate_pem: dsh_config.dsh_ca_certificate().to_string(),
69-
dsh_kafka_certificate_pem,
70-
key_pair: Arc::new(key_pair),
71-
})
83+
Ok(Self::new(
84+
dsh_config.dsh_ca_certificate().to_string(),
85+
client_certificate,
86+
key_pair,
87+
))
7288
}
7389

7490
/// Build an async reqwest client with the DSH Kafka certificate included.
@@ -108,7 +124,7 @@ impl Cert {
108124

109125
/// Get the kafka certificate as PEM string. Equivalent to client.pem.
110126
pub fn dsh_kafka_certificate_pem(&self) -> &str {
111-
self.dsh_kafka_certificate_pem.as_str()
127+
&self.dsh_client_certificate_pem.as_str()
112128
}
113129

114130
/// Get the private key as PKCS8 and return bytes based on asn1 DER format.
@@ -200,7 +216,7 @@ mod tests {
200216
fn set_test_cert() -> Cert {
201217
Cert {
202218
dsh_ca_certificate_pem: CA_CERT.to_string(),
203-
dsh_kafka_certificate_pem: KAFKA_CERT.to_string(),
219+
dsh_client_certificate_pem: KAFKA_CERT.to_string(),
204220
key_pair: Arc::new(KeyPair::generate().unwrap()),
205221
}
206222
}

0 commit comments

Comments
 (0)