use std::error;
use std::ffi::OsStr;
use std::fmt::{Display, Formatter, Result as FResult};
use std::path::{Path, PathBuf};
use std::time::Duration;
use brane_cfg::info::Info;
use brane_cfg::node::{NodeConfig, NodeSpecificConfig, WorkerConfig};
use brane_shr::formatters::BlockFormatter;
use console::style;
use dialoguer::theme::ColorfulTheme;
use enum_debug::EnumDebug;
use error_trace::trace;
use log::{debug, info};
use policy::{Policy, PolicyVersion};
use rand::Rng;
use rand::distributions::Alphanumeric;
use reqwest::{Client, Request, Response, StatusCode};
use serde_json::value::RawValue;
use specifications::address::{Address, AddressOpt};
use specifications::checking::{
POLICY_API_ADD_VERSION, POLICY_API_GET_ACTIVE_VERSION, POLICY_API_GET_VERSION, POLICY_API_LIST_POLICIES, POLICY_API_SET_ACTIVE_VERSION,
};
use srv::models::{AddPolicyPostModel, PolicyContentPostModel, SetVersionPostModel};
use tokio::fs::{self as tfs, File as TFile};
use crate::spec::PolicyInputLanguage;
#[derive(Debug)]
pub enum Error {
ActiveVersionGet { addr: Address, err: Box<Self> },
InputDeserialize { path: PathBuf, raw: String, err: serde_json::Error },
InputRead { path: PathBuf, err: std::io::Error },
InputToJson { path: PathBuf, err: eflint_to_json::Error },
InvalidPolicyActivated { addr: Address, got: Option<i64>, expected: Option<i64> },
MissingExtension { path: PathBuf },
NodeConfigIncompatible { path: PathBuf, got: String },
NodeConfigLoad { path: PathBuf, err: brane_cfg::info::YamlError },
PolicyWithoutVersion { addr: Address, which: String },
PromptVersions { err: Box<Self> },
RequestBuild { kind: &'static str, addr: String, err: reqwest::Error },
RequestFailure { addr: String, code: StatusCode, response: Option<String> },
RequestSend { kind: &'static str, addr: String, err: reqwest::Error },
ResponseDeserialize { addr: String, raw: String, err: serde_json::Error },
ResponseDownload { addr: String, err: reqwest::Error },
TempFileCreate { path: PathBuf, err: std::io::Error },
TempFileWrite { path: PathBuf, err: std::io::Error },
TokenGenerate { secret: PathBuf, err: specifications::policy::Error },
UnknownExtension { path: PathBuf, ext: String },
UnspecifiedInputLanguage,
VersionGetBody { addr: Address, version: i64, err: Box<Self> },
VersionSelect { err: dialoguer::Error },
VersionsGet { addr: Address, err: Box<Self> },
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> FResult {
use Error::*;
match self {
ActiveVersionGet { addr, .. } => write!(f, "Failed to get active version of checker '{addr}'"),
InputDeserialize { path, raw, .. } => {
write!(f, "Failed to deserialize contents of '{}' to JSON\n\nRaw value:\n{}\n", path.display(), BlockFormatter::new(raw))
},
InputRead { path, .. } => write!(f, "Failed to read input file '{}'", path.display()),
InputToJson { path, .. } => write!(f, "Failed to compile input file '{}' to eFLINT JSON", path.display()),
InvalidPolicyActivated { addr, got, expected } => write!(
f,
"Checker '{}' activated wrong policy; it says it activated {}, but we requested to activate {}",
addr,
if let Some(got) = got { got.to_string() } else { "None".into() },
if let Some(expected) = expected { expected.to_string() } else { "None".into() }
),
MissingExtension { path } => {
write!(f, "Cannot derive input language from '{}' that has no extension; manually specify it using '--language'", path.display())
},
NodeConfigIncompatible { path, got } => {
write!(f, "Given node configuration file '{}' is for a {} node, but expected a Worker node", path.display(), got)
},
NodeConfigLoad { path, .. } => write!(f, "Failed to load node configuration file '{}'", path.display()),
PolicyWithoutVersion { addr, which } => write!(f, "{which} policy return by checker '{addr}' has no version number set"),
PromptVersions { .. } => write!(f, "Failed to prompt the user (you!) to select a version"),
RequestBuild { kind, addr, .. } => write!(f, "Failed to build new {kind}-request to '{addr}'"),
RequestFailure { addr, code, response } => write!(
f,
"Request to '{}' failed with status {} ({}){}",
addr,
code.as_u16(),
code.canonical_reason().unwrap_or("???"),
if let Some(response) = response { format!("\n\nResponse:\n{}\n", BlockFormatter::new(response)) } else { String::new() }
),
RequestSend { kind, addr, .. } => write!(f, "Failed to send {kind}-request to '{addr}'"),
ResponseDeserialize { addr, raw, .. } => {
write!(f, "Failed to deserialize response from '{}' as JSON\n\nResponse:\n{}\n", addr, BlockFormatter::new(raw))
},
ResponseDownload { addr, .. } => write!(f, "Failed to download response from '{addr}'"),
TempFileCreate { path, .. } => write!(f, "Failed to create temporary file '{}'", path.display()),
TempFileWrite { path, .. } => write!(f, "Failed to copy stdin to temporary file '{}'", path.display()),
TokenGenerate { secret, .. } => write!(
f,
"Failed to generate one-time authentication token from secret file '{}' (you can manually specify a token using '--token')",
secret.display()
),
UnknownExtension { path, ext } => write!(
f,
"Cannot derive input language from '{}' that has unknown extension '{}'; manually specify it using '--language'",
path.display(),
ext
),
UnspecifiedInputLanguage => write!(f, "Cannot derive input language when giving input via stdin; manually specify it using '--language'"),
VersionGetBody { addr, version, .. } => write!(f, "Failed to get policy body of policy '{version}' stored in checker '{addr}'"),
VersionSelect { .. } => write!(f, "Failed to ask you which version to make active"),
VersionsGet { addr, .. } => write!(f, "Failed to get policy versions stored in checker '{addr}'"),
}
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
use Error::*;
match self {
ActiveVersionGet { err, .. } => Some(&**err),
InputDeserialize { err, .. } => Some(err),
InputRead { err, .. } => Some(err),
InputToJson { err, .. } => Some(err),
InvalidPolicyActivated { .. } => None,
MissingExtension { .. } => None,
NodeConfigIncompatible { .. } => None,
NodeConfigLoad { err, .. } => Some(err),
PolicyWithoutVersion { .. } => None,
PromptVersions { err } => Some(err),
RequestBuild { err, .. } => Some(err),
RequestFailure { .. } => None,
RequestSend { err, .. } => Some(err),
ResponseDeserialize { err, .. } => Some(err),
ResponseDownload { err, .. } => Some(err),
TempFileCreate { err, .. } => Some(err),
TempFileWrite { err, .. } => Some(err),
TokenGenerate { err, .. } => Some(err),
UnknownExtension { .. } => None,
UnspecifiedInputLanguage => None,
VersionGetBody { err, .. } => Some(&**err),
VersionSelect { err } => Some(err),
VersionsGet { err, .. } => Some(&**err),
}
}
}
fn resolve_worker_config(node_config_path: impl AsRef<Path>, worker: Option<WorkerConfig>) -> Result<WorkerConfig, Error> {
worker.map(Ok).unwrap_or_else(|| {
let node_config_path: &Path = node_config_path.as_ref();
debug!("Loading node configuration file '{}'...", node_config_path.display());
let node: NodeConfig = match NodeConfig::from_path(node_config_path) {
Ok(node) => node,
Err(err) => return Err(Error::NodeConfigLoad { path: node_config_path.into(), err }),
};
match node.node {
NodeSpecificConfig::Worker(worker) => Ok(worker),
other => Err(Error::NodeConfigIncompatible { path: node_config_path.into(), got: other.variant().to_string() }),
}
})
}
fn resolve_token(node_config_path: impl AsRef<Path>, worker: &mut Option<WorkerConfig>, token: Option<String>) -> Result<String, Error> {
if let Some(token) = token {
debug!("Using given token '{token}'");
Ok(token)
} else {
let worker_cfg: WorkerConfig = resolve_worker_config(&node_config_path, worker.take())?;
match specifications::policy::generate_policy_token(
names::three::lowercase::rand(),
"branectl",
Duration::from_secs(60),
&worker_cfg.paths.policy_expert_secret,
) {
Ok(token) => {
debug!("Using generated token '{token}'");
*worker = Some(worker_cfg);
Ok(token)
},
Err(err) => Err(Error::TokenGenerate { secret: worker_cfg.paths.policy_expert_secret, err }),
}
}
}
fn resolve_addr_opt(node_config_path: impl AsRef<Path>, worker: &mut Option<WorkerConfig>, mut address: AddressOpt) -> Result<Address, Error> {
if address.port().is_none() {
let worker_cfg: WorkerConfig = resolve_worker_config(&node_config_path, worker.take())?;
*address.port_mut() = Some(worker_cfg.services.chk.address.port());
*worker = Some(worker_cfg);
}
Ok(Address::try_from(address).unwrap())
}
async fn get_version_body_from_checker(address: &Address, token: &str, version: i64) -> Result<Policy, Error> {
info!("Retrieving policy '{version}' from checker '{address}'");
let url: String = format!("http://{}/{}", address, POLICY_API_GET_VERSION.1(version));
debug!("Building GET-request to '{url}'...");
let client: Client = Client::new();
let req: Request = match client.request(POLICY_API_GET_VERSION.0, &url).bearer_auth(token).build() {
Ok(req) => req,
Err(err) => return Err(Error::RequestBuild { kind: "GET", addr: url, err }),
};
debug!("Sending request to '{url}'...");
let res: Response = match client.execute(req).await {
Ok(res) => res,
Err(err) => return Err(Error::RequestSend { kind: "GET", addr: url, err }),
};
debug!("Server responded with {}", res.status());
if !res.status().is_success() {
return Err(Error::RequestFailure { addr: url, code: res.status(), response: res.text().await.ok() });
}
match res.text().await {
Ok(body) => {
debug!("Response:\n{}\n", BlockFormatter::new(&body));
match serde_json::from_str(&body) {
Ok(body) => Ok(body),
Err(err) => Err(Error::ResponseDeserialize { addr: url, raw: body, err }),
}
},
Err(err) => Err(Error::ResponseDownload { addr: url, err }),
}
}
async fn get_versions_on_checker(address: &Address, token: &str) -> Result<Vec<PolicyVersion>, Error> {
info!("Retrieving policies on checker '{address}'");
let url: String = format!("http://{}/{}", address, POLICY_API_LIST_POLICIES.1);
debug!("Building GET-request to '{url}'...");
let client: Client = Client::new();
let req: Request = match client.request(POLICY_API_LIST_POLICIES.0, &url).bearer_auth(token).build() {
Ok(req) => req,
Err(err) => return Err(Error::RequestBuild { kind: "GET", addr: url, err }),
};
debug!("Sending request to '{url}'...");
let res: Response = match client.execute(req).await {
Ok(res) => res,
Err(err) => return Err(Error::RequestSend { kind: "GET", addr: url, err }),
};
debug!("Server responded with {}", res.status());
if !res.status().is_success() {
return Err(Error::RequestFailure { addr: url, code: res.status(), response: res.text().await.ok() });
}
match res.text().await {
Ok(body) => {
debug!("Response:\n{}\n", BlockFormatter::new(&body));
match serde_json::from_str(&body) {
Ok(body) => Ok(body),
Err(err) => Err(Error::ResponseDeserialize { addr: url, raw: body, err }),
}
},
Err(err) => Err(Error::ResponseDownload { addr: url, err }),
}
}
async fn get_active_version_on_checker(address: &Address, token: &str) -> Result<Option<Policy>, Error> {
info!("Retrieving active policy of checker '{address}'");
let url: String = format!("http://{}/{}", address, POLICY_API_GET_ACTIVE_VERSION.1);
debug!("Building GET-request to '{url}'...");
let client: Client = Client::new();
let req: Request = match client.request(POLICY_API_GET_ACTIVE_VERSION.0, &url).bearer_auth(token).build() {
Ok(req) => req,
Err(err) => return Err(Error::RequestBuild { kind: "GET", addr: url, err }),
};
debug!("Sending request to '{url}'...");
let res: Response = match client.execute(req).await {
Ok(res) => res,
Err(err) => return Err(Error::RequestSend { kind: "GET", addr: url, err }),
};
debug!("Server responded with {}", res.status());
match res.status() {
StatusCode::OK => {},
StatusCode::NOT_FOUND => return Ok(None),
code => return Err(Error::RequestFailure { addr: url, code, response: res.text().await.ok() }),
}
match res.text().await {
Ok(body) => {
debug!("Response:\n{}\n", BlockFormatter::new(&body));
match serde_json::from_str(&body) {
Ok(body) => Ok(body),
Err(err) => Err(Error::ResponseDeserialize { addr: url, raw: body, err }),
}
},
Err(err) => Err(Error::ResponseDownload { addr: url, err }),
}
}
fn prompt_user_version(
address: impl Into<Address>,
active_version: Option<i64>,
versions: &[PolicyVersion],
exit: bool,
) -> Result<Option<usize>, Error> {
let mut sversions: Vec<String> = Vec::with_capacity(versions.len() + 1);
for (i, version) in versions.iter().enumerate() {
if version.version.is_none() {
return Err(Error::PolicyWithoutVersion { addr: address.into(), which: format!("{i}th") });
}
let mut line: String = if version.version == active_version { style("Version ").bold().to_string() } else { "Version ".into() };
line.push_str(&style(version.version.unwrap()).bold().green().to_string());
if version.version == active_version {
line.push_str(
&style(format!(
" (created at {}, by {})",
version.created_at.format("%H:%M:%S %d-%m-%Y"),
version.creator.as_deref().unwrap_or("<unknown>")
))
.to_string(),
);
} else {
line.push_str(&format!(
" (created at {}, by {})",
version.created_at.format("%H:%M:%S %d-%m-%Y"),
version.creator.as_deref().unwrap_or("<unknown>")
));
}
sversions.push(line);
}
if exit {
sversions.push("<exit>".into());
}
match dialoguer::Select::with_theme(&ColorfulTheme::default())
.with_prompt("Which version do you want to make active?")
.items(&sversions)
.interact()
{
Ok(idx) => {
if !exit || idx < versions.len() {
Ok(Some(idx))
} else {
Ok(None)
}
},
Err(err) => Err(Error::VersionSelect { err }),
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum TargetReasoner {
EFlintJson(EFlintJsonVersion),
}
impl TargetReasoner {
pub fn id(&self) -> String {
match self {
Self::EFlintJson(_) => "eflint-json".into(),
}
}
pub fn version(&self) -> String {
match self {
Self::EFlintJson(v) => v.to_string(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum EFlintJsonVersion {
V0_1_0,
}
impl Display for EFlintJsonVersion {
fn fmt(&self, f: &mut Formatter<'_>) -> FResult {
match self {
Self::V0_1_0 => write!(f, "0.1.0"),
}
}
}
pub async fn activate(node_config_path: PathBuf, version: Option<i64>, address: AddressOpt, token: Option<String>) -> Result<(), Error> {
info!(
"Activating policy{} on checker of node defined by '{}'",
if let Some(version) = &version { format!(" version '{version}'") } else { String::new() },
node_config_path.display()
);
let mut worker: Option<WorkerConfig> = None;
let token: String = resolve_token(&node_config_path, &mut worker, token)?;
let address: Address = resolve_addr_opt(&node_config_path, &mut worker, address)?;
let version: i64 = if let Some(version) = version {
version
} else {
let mut versions: Vec<PolicyVersion> = match get_versions_on_checker(&address, &token).await {
Ok(versions) => versions,
Err(err) => return Err(Error::VersionsGet { addr: address, err: Box::new(err) }),
};
let active_version: Option<i64> = match get_active_version_on_checker(&address, &token).await {
Ok(version) => version.and_then(|v| v.version.version),
Err(err) => return Err(Error::ActiveVersionGet { addr: address, err: Box::new(err) }),
};
let idx: usize = match prompt_user_version(&address, active_version, &versions, false) {
Ok(Some(idx)) => idx,
Ok(None) => unreachable!(),
Err(err) => return Err(Error::PromptVersions { err: Box::new(err) }),
};
versions.swap_remove(idx).version.unwrap()
};
debug!("Activating policy version {version}");
let url: String = format!("http://{}/{}", address, POLICY_API_SET_ACTIVE_VERSION.1);
debug!("Building PUT-request to '{url}'...");
let client: Client = Client::new();
let req: Request = match client.request(POLICY_API_SET_ACTIVE_VERSION.0, &url).bearer_auth(token).json(&SetVersionPostModel { version }).build() {
Ok(req) => req,
Err(err) => return Err(Error::RequestBuild { kind: "GET", addr: url, err }),
};
debug!("Sending request to '{url}'...");
let res: Response = match client.execute(req).await {
Ok(res) => res,
Err(err) => return Err(Error::RequestSend { kind: "GET", addr: url, err }),
};
debug!("Server responded with {}", res.status());
if !res.status().is_success() {
return Err(Error::RequestFailure { addr: url, code: res.status(), response: res.text().await.ok() });
}
let res: Policy = match res.text().await {
Ok(body) => {
debug!("Response:\n{}\n", BlockFormatter::new(&body));
match serde_json::from_str(&body) {
Ok(body) => body,
Err(err) => return Err(Error::ResponseDeserialize { addr: url, raw: body, err }),
}
},
Err(err) => return Err(Error::ResponseDownload { addr: url, err }),
};
if res.version.version != Some(version) {
return Err(Error::InvalidPolicyActivated { addr: address, got: res.version.version, expected: Some(version) });
}
println!("Successfully activated policy {} to checker {}.", style(version).bold().green(), style(address).bold().green(),);
Ok(())
}
pub async fn add(
node_config_path: PathBuf,
input: String,
language: Option<PolicyInputLanguage>,
address: AddressOpt,
token: Option<String>,
) -> Result<(), Error> {
info!("Adding policy '{}' to checker of node defined by '{}'", input, node_config_path.display());
let mut worker: Option<WorkerConfig> = None;
let token: String = resolve_token(&node_config_path, &mut worker, token)?;
let address: Address = resolve_addr_opt(&node_config_path, &mut worker, address)?;
let (input, from_stdin): (PathBuf, bool) = if input == "-" {
let id: String = rand::thread_rng().sample_iter(Alphanumeric).take(4).map(char::from).collect::<String>();
let temp_path: PathBuf = std::env::temp_dir().join(format!("branectl-stdin-{id}.txt"));
debug!("Writing stdin to temporary file '{}'...", temp_path.display());
let mut temp: TFile = match TFile::create(&temp_path).await {
Ok(temp) => temp,
Err(err) => return Err(Error::TempFileCreate { path: temp_path, err }),
};
if let Err(err) = tokio::io::copy(&mut tokio::io::stdin(), &mut temp).await {
return Err(Error::TempFileWrite { path: temp_path, err });
}
(temp_path, true)
} else {
(input.into(), false)
};
let language: PolicyInputLanguage = if let Some(language) = language {
debug!("Interpreting input as {language}");
language
} else if let Some(ext) = input.extension() {
debug!("Attempting to derive input language from extension '{}' (part of '{}')", ext.to_string_lossy(), input.display());
if ext == OsStr::new("eflint") {
PolicyInputLanguage::EFlint
} else if ext == OsStr::new("json") {
PolicyInputLanguage::EFlintJson
} else if from_stdin {
return Err(Error::UnspecifiedInputLanguage);
} else {
let ext: String = ext.to_string_lossy().into();
return Err(Error::UnknownExtension { path: input, ext });
}
} else if from_stdin {
return Err(Error::UnspecifiedInputLanguage);
} else {
return Err(Error::MissingExtension { path: input });
};
let (json, target_reasoner): (String, TargetReasoner) = match language {
PolicyInputLanguage::EFlint => {
debug!("Compiling eFLINT input file '{}' to eFLINT JSON", input.display());
let mut json: Vec<u8> = Vec::new();
if let Err(err) = eflint_to_json::compile_async(&input, &mut json, None).await {
return Err(Error::InputToJson { path: input, err });
}
match String::from_utf8(json) {
Ok(json) => (json, TargetReasoner::EFlintJson(EFlintJsonVersion::V0_1_0)),
Err(err) => panic!("{}", trace!(("eflint_to_json::compile_async() did not return valid UTF-8"), err)),
}
},
PolicyInputLanguage::EFlintJson => {
debug!("Reading eFLINT JSON input file '{}'", input.display());
match tfs::read_to_string(&input).await {
Ok(json) => (json, TargetReasoner::EFlintJson(EFlintJsonVersion::V0_1_0)),
Err(err) => return Err(Error::InputRead { path: input, err }),
}
},
};
debug!("Deserializing input as JSON...");
let json: Box<RawValue> = match serde_json::from_str(&json) {
Ok(json) => json,
Err(err) => return Err(Error::InputDeserialize { path: input, raw: json, err }),
};
let url: String = format!("http://{}/{}", address, POLICY_API_ADD_VERSION.1);
debug!("Building POST-request to '{url}'...");
let client: Client = Client::new();
let contents: AddPolicyPostModel = AddPolicyPostModel {
version_description: "".into(),
description: None,
content: vec![PolicyContentPostModel { reasoner: target_reasoner.id(), reasoner_version: target_reasoner.version(), content: json }],
};
let req: Request = match client.request(POLICY_API_ADD_VERSION.0, &url).bearer_auth(token).json(&contents).build() {
Ok(req) => req,
Err(err) => return Err(Error::RequestBuild { kind: "POST", addr: url, err }),
};
debug!("Sending request to '{url}'...");
let res: Response = match client.execute(req).await {
Ok(res) => res,
Err(err) => return Err(Error::RequestSend { kind: "POST", addr: url, err }),
};
debug!("Server responded with {}", res.status());
if !res.status().is_success() {
return Err(Error::RequestFailure { addr: url, code: res.status(), response: res.text().await.ok() });
}
let body: Policy = match res.text().await {
Ok(body) => {
debug!("Response:\n{}\n", BlockFormatter::new(&body));
match serde_json::from_str(&body) {
Ok(body) => body,
Err(err) => return Err(Error::ResponseDeserialize { addr: url, raw: body, err }),
}
},
Err(err) => return Err(Error::ResponseDownload { addr: url, err }),
};
println!(
"Successfully added policy {} to checker {}{}.",
style(if from_stdin { "<stdin>".into() } else { input.display().to_string() }).bold().green(),
style(address).bold().green(),
if let Some(version) = body.version.version { format!(" as version {}", style(version).bold().green()) } else { String::new() }
);
Ok(())
}
pub async fn list(node_config_path: PathBuf, address: AddressOpt, token: Option<String>) -> Result<(), Error> {
info!("Listing policy on checker of node defined by '{}'", node_config_path.display());
let mut worker: Option<WorkerConfig> = None;
let token: String = resolve_token(&node_config_path, &mut worker, token)?;
let address: Address = resolve_addr_opt(&node_config_path, &mut worker, address)?;
let mut versions: Vec<PolicyVersion> = match get_versions_on_checker(&address, &token).await {
Ok(versions) => versions,
Err(err) => return Err(Error::VersionsGet { addr: address, err: Box::new(err) }),
};
let active_version: Option<i64> = match get_active_version_on_checker(&address, &token).await {
Ok(version) => version.and_then(|v| v.version.version),
Err(err) => return Err(Error::ActiveVersionGet { addr: address, err: Box::new(err) }),
};
loop {
let idx: usize = match prompt_user_version(&address, active_version, &versions, true) {
Ok(Some(idx)) => idx,
Ok(None) => break,
Err(err) => return Err(Error::PromptVersions { err: Box::new(err) }),
};
let version: i64 = versions.swap_remove(idx).version.unwrap();
let _version: Policy = match get_version_body_from_checker(&address, &token, version).await {
Ok(version) => version,
Err(err) => return Err(Error::VersionGetBody { addr: address, version, err: Box::new(err) }),
};
}
todo!();
}