use btconfig::{
get_setting, get_settings, CredStoreConfig, CredStoreConsumer, CredStoreMutConsumer, FigmentExt,
};
use btlib::{
bterr,
crypto::{CredStore, CredStoreMut, Creds},
BlockPath, Epoch, Principal, Principaled, Result, Writecap,
};
use btserde::{read_from, write_to};
use figment::{providers::Serialized, Figment};
use serde::{Deserialize, Serialize};
use std::{
fs::OpenOptions,
io::{BufReader, BufWriter, Cursor, Write},
path::PathBuf,
time::Duration,
};
use tempdir::TempDir;
use termion::input::TermRead;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BtprovisionConfig {
#[serde(rename = "credstore")]
pub cred_store: CredStoreConfig,
pub password: Option<String>,
#[serde(rename = "writecapexpires")]
pub writecap_expires: Option<u64>,
#[serde(rename = "writecappath")]
pub writecap_path: Option<String>,
#[serde(rename = "writecapissuee")]
pub writecap_issuee: Option<String>,
#[serde(rename = "writecapsavepath")]
pub writecap_save_path: Option<PathBuf>,
}
impl BtprovisionConfig {
pub fn new() -> Result<Self> {
Figment::new()
.merge(Serialized::defaults(BtprovisionConfig::default()))
.btconfig()?
.extract()
.map_err(|err| err.into())
}
}
impl Default for BtprovisionConfig {
fn default() -> Self {
const DEFAULT_VALID_FOR: u64 = 60 * 60 * 24 * 365;
let expires = Epoch::now() + Duration::from_secs(DEFAULT_VALID_FOR);
Self {
cred_store: CredStoreConfig::default(),
password: None,
writecap_expires: Some(expires.value()),
writecap_path: None,
writecap_issuee: None,
writecap_save_path: None,
}
}
}
fn password_prompt(prompt: &str) -> Result<String> {
let mut stdout = std::io::stdout();
stdout.write_all(prompt.as_bytes())?;
stdout.flush()?;
let mut reader = BufReader::new(std::io::stdin());
let mut cursor = Cursor::new(Vec::new());
let line = if let Some(line) = reader.read_passwd(&mut cursor)? {
line
} else {
return Err(bterr!("failed to read password"));
};
writeln!(stdout)?;
stdout.flush()?;
Ok(line)
}
struct RootProvisionConsumer<'a> {
password: &'a str,
expires: Epoch,
}
impl<'a> CredStoreMutConsumer for RootProvisionConsumer<'a> {
type Output = Result<()>;
fn consume_mut<C: CredStoreMut>(self, cred_store: C) -> Self::Output {
cred_store.provision_root(self.password, self.expires)?;
Ok(())
}
}
fn gen_root_creds(config: BtprovisionConfig) -> Result<()> {
let password = if let Some(password) = config.password {
password
} else {
let password = password_prompt("Please enter a root password: ")?;
let password_confirm = password_prompt("Please confirm your entry: ")?;
if password != password_confirm {
return Err(bterr!("Error: entries do not match"));
}
password
};
let expires = get_setting!(config, writecap_expires);
config.cred_store.consume_mut(RootProvisionConsumer {
password: &password,
expires: Epoch::from_value(expires),
})?
}
fn gen_node_creds(config: BtprovisionConfig) -> Result<()> {
let principal = config.cred_store.consume(PrincipalConsumer)??;
eprint!("node principal: ");
println!("{principal}");
Ok(())
}
struct IssueWritecapConsumer<'a> {
password: &'a str,
issuee: Principal,
components: String,
expires: Epoch,
}
impl<'a> CredStoreConsumer for IssueWritecapConsumer<'a> {
type Output = Result<Writecap>;
fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
let root_creds = cred_store.root_creds(self.password)?;
let mut components = self.components.split(BlockPath::SEP);
root_creds.issue_writecap(self.issuee, &mut components, self.expires)
}
}
fn issue_node_writecap(config: BtprovisionConfig) -> Result<()> {
let (writecap_path, issuee, expires) = get_settings!(
config,
writecap_save_path,
writecap_issuee,
writecap_expires
);
let password = if let Some(password) = config.password {
password
} else {
password_prompt("Please enter the root password: ")?
};
let cred_components = if let Some(cred_path) = config.writecap_path {
cred_path
} else {
String::new()
};
let issuee = Principal::try_from(issuee.as_str())?;
let writecap = config.cred_store.consume(IssueWritecapConsumer {
password: &password,
components: cred_components,
expires: Epoch::from_value(expires),
issuee,
})??;
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&writecap_path)
.map_err(|err| {
bterr!(
"failed to create writecap at path '{}': {err}",
writecap_path.display()
)
})?;
let mut writer = BufWriter::new(file);
write_to(&writecap, &mut writer).map_err(|err| bterr!("failed to save writecap: {err}"))?;
Ok(())
}
struct SaveNodeWritecapConsumer {
writecap: Writecap,
}
impl CredStoreMutConsumer for SaveNodeWritecapConsumer {
type Output = Result<()>;
fn consume_mut<C: CredStoreMut>(self, cred_store: C) -> Self::Output {
let mut node_creds = cred_store.node_creds()?;
cred_store.assign_node_writecap(&mut node_creds, self.writecap)?;
Ok(())
}
}
fn save_node_writecap(config: BtprovisionConfig) -> Result<()> {
let writecap = {
let writecap_path = get_setting!(config, writecap_save_path);
let file = OpenOptions::new()
.read(true)
.write(false)
.create(false)
.open(writecap_path)?;
let mut reader = BufReader::new(file);
read_from::<Writecap, _>(&mut reader)?
};
config
.cred_store
.consume_mut(SaveNodeWritecapConsumer { writecap })?
}
struct PrincipalConsumer;
impl CredStoreConsumer for PrincipalConsumer {
type Output = Result<Principal>;
fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
Ok(cred_store.node_creds()?.principal())
}
}
fn full(mut config: BtprovisionConfig) -> Result<()> {
gen_root_creds(config.clone())?;
gen_node_creds(config.clone())?;
let node_principal = config.cred_store.clone().consume(PrincipalConsumer)??;
config.writecap_issuee = Some(node_principal.to_string());
let _temp_dir = if config.writecap_save_path.is_none() {
let temp_dir = TempDir::new("btprovision")?;
config.writecap_save_path = Some(temp_dir.path().join("writecap"));
Some(temp_dir)
} else {
None
};
issue_node_writecap(config.clone())?;
save_node_writecap(config)
}
fn run(command: &str, config: BtprovisionConfig) -> Result<()> {
match command {
"gen_root_creds" => gen_root_creds(config),
"gen_node_creds" => gen_node_creds(config),
"issue_node_writecap" => issue_node_writecap(config),
"save_node_writecap" => save_node_writecap(config),
"full" => full(config),
_ => Err(bterr!("unrecognized command: {command}")),
}
}
fn main() -> Result<()> {
let config = BtprovisionConfig::new()?;
let mut args = std::env::args().skip(1);
let command = args
.next()
.ok_or_else(|| bterr!("at least one command line argument expected"))?;
run(command.as_str(), config)
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use btconfig::CredStoreConfig;
use btlib::{crypto::CredsPriv, BlockError, RelBlockPath};
use tempdir::TempDir;
struct NodeWritecapConsumer;
impl CredStoreConsumer for NodeWritecapConsumer {
type Output = Writecap;
fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
let node_creds = cred_store.node_creds().unwrap();
node_creds
.writecap()
.ok_or(BlockError::MissingWritecap)
.unwrap()
.clone()
}
}
struct RootWritecapConsumer<'a> {
password: &'a str,
}
impl<'a> CredStoreConsumer for RootWritecapConsumer<'a> {
type Output = Writecap;
fn consume<C: CredStore>(self, cred_store: C) -> Self::Output {
let root_creds = cred_store.root_creds(self.password).unwrap();
root_creds
.writecap()
.ok_or(BlockError::MissingWritecap)
.unwrap()
.clone()
}
}
#[test]
fn workflow() {
let expires_expected = (Epoch::now() + Duration::from_secs(7200)).value();
let writecap_path = "home/gboole".to_string();
let password = "devalued".to_string();
let dir = TempDir::new("btprovision").unwrap();
let writecap_save_path = dir.path().join("writecap");
let root_store = CredStoreConfig::File {
path: dir.path().join("root_store"),
};
let node_store = CredStoreConfig::File {
path: dir.path().join("node_store"),
};
run(
"gen_root_creds",
BtprovisionConfig {
cred_store: root_store.clone(),
password: Some(password.clone()),
writecap_expires: Some(expires_expected),
writecap_path: None,
writecap_issuee: None,
writecap_save_path: None,
},
)
.unwrap();
{
let consumer = RootWritecapConsumer {
password: &password,
};
let actual = root_store.clone().consume(consumer).unwrap();
actual.assert_valid_for(actual.path()).unwrap();
actual.assert_issued_to(&actual.root_principal()).unwrap();
assert_eq!(expires_expected, actual.expires().value());
assert!(std::iter::empty::<&str>().eq(actual.path().components()));
}
run(
"gen_node_creds",
BtprovisionConfig {
cred_store: node_store.clone(),
password: None,
writecap_expires: None,
writecap_path: None,
writecap_issuee: None,
writecap_save_path: None,
},
)
.unwrap();
let issued_to_expected = node_store
.clone()
.consume(PrincipalConsumer)
.unwrap()
.unwrap()
.to_string();
run(
"issue_node_writecap",
BtprovisionConfig {
cred_store: root_store,
password: Some(password),
writecap_expires: Some(expires_expected),
writecap_path: Some(writecap_path.clone()),
writecap_issuee: Some(issued_to_expected.clone()),
writecap_save_path: Some(writecap_save_path.clone()),
},
)
.unwrap();
run(
"save_node_writecap",
BtprovisionConfig {
cred_store: node_store.clone(),
password: None,
writecap_expires: None,
writecap_path: None,
writecap_issuee: None,
writecap_save_path: Some(writecap_save_path),
},
)
.unwrap();
{
let actual = node_store.consume(NodeWritecapConsumer).unwrap();
actual.assert_valid_for(actual.path()).unwrap();
assert_eq!(issued_to_expected, actual.issued_to().to_string());
let actual_path = actual
.path()
.relative_to(&actual.root_block_path())
.unwrap();
let expected_path = RelBlockPath::try_from(writecap_path.as_str()).unwrap();
assert_eq!(expected_path, actual_path);
assert_eq!(expires_expected, actual.expires().value());
}
}
}
#[cfg(test)]
mod config_tests {
use super::BtprovisionConfig;
use std::path::PathBuf;
use figment::Jail;
macro_rules! test_field {
($field:ident, $expected:expr, $env_var:literal) => {
#[test]
fn $field() {
Jail::expect_with(|jail| {
jail.set_env($env_var, $expected);
let config = BtprovisionConfig::new().unwrap();
assert_eq!(Some($expected), config.$field);
Ok(())
})
}
};
}
test_field!(password, String::from("eldritch"), "BT_PASSWORD");
test_field!(writecap_expires, 1729, "BT_WRITECAPEXPIRES");
test_field!(
writecap_path,
String::from("foofercoorg"),
"BT_WRITECAPPATH"
);
test_field!(
writecap_issuee,
String::from("slammin"),
"BT_WRITECAPISSUEE"
);
#[test]
fn writecap_save_path() {
Jail::expect_with(|jail| {
let expected = PathBuf::from("./writecap");
jail.set_env("BT_WRITECAPSAVEPATH", expected.display());
let config = BtprovisionConfig::new().unwrap();
assert_eq!(Some(expected), config.writecap_save_path);
Ok(())
})
}
}