use std::time::Duration;
use kovra_core::{Biometric, ConfirmOutcome, ConfirmRequest, Confirmer};
pub mod formatter;
pub mod render;
pub use formatter::DiskutilFormatter;
#[cfg(target_os = "macos")]
mod macos;
#[must_use]
pub fn biometrics_available() -> bool {
#[cfg(target_os = "macos")]
{
macos::can_evaluate()
}
#[cfg(not(target_os = "macos"))]
{
false
}
}
pub struct NativeBiometric {
#[cfg(target_os = "macos")]
inner: macos::MacBiometric,
}
impl NativeBiometric {
#[must_use]
pub fn new() -> Self {
Self {
#[cfg(target_os = "macos")]
inner: macos::MacBiometric::new(),
}
}
}
impl Default for NativeBiometric {
fn default() -> Self {
Self::new()
}
}
impl Biometric for NativeBiometric {
fn prompt(&self, req: &ConfirmRequest, timeout: Duration) -> ConfirmOutcome {
#[cfg(target_os = "macos")]
{
self.inner.prompt(req, timeout)
}
#[cfg(not(target_os = "macos"))]
{
let _ = (req, timeout);
ConfirmOutcome::Denied
}
}
}
pub struct BiometricConfirmer<B: Biometric = NativeBiometric> {
biometric: B,
}
impl BiometricConfirmer<NativeBiometric> {
#[must_use]
pub fn new() -> Self {
Self {
biometric: NativeBiometric::new(),
}
}
}
impl Default for BiometricConfirmer<NativeBiometric> {
fn default() -> Self {
Self::new()
}
}
impl<B: Biometric> BiometricConfirmer<B> {
pub fn with_biometric(biometric: B) -> Self {
Self { biometric }
}
}
impl<B: Biometric> Confirmer for BiometricConfirmer<B> {
fn confirm(&self, req: &ConfirmRequest, timeout: Duration) -> ConfirmOutcome {
self.biometric.prompt(req, timeout)
}
}
#[cfg(test)]
mod tests {
use super::*;
use kovra_core::{Origin, Sensitivity};
use std::cell::Cell;
fn req() -> ConfirmRequest {
ConfirmRequest::new("prod/db/password", Sensitivity::High, "prod", Origin::Agent)
.with_command("/usr/bin/deploy --env prod")
}
struct MockBiometric {
outcome: ConfirmOutcome,
prompted: Cell<u32>,
last_text: std::cell::RefCell<Option<String>>,
}
impl MockBiometric {
fn new(outcome: ConfirmOutcome) -> Self {
Self {
outcome,
prompted: Cell::new(0),
last_text: std::cell::RefCell::new(None),
}
}
}
impl Biometric for MockBiometric {
fn prompt(&self, req: &ConfirmRequest, _timeout: Duration) -> ConfirmOutcome {
self.prompted.set(self.prompted.get() + 1);
*self.last_text.borrow_mut() = Some(render::prompt_text(req));
self.outcome
}
}
#[test]
fn i3_high_prod_drives_confirm_and_outcome_gates_delivery() {
for outcome in [
ConfirmOutcome::Approved,
ConfirmOutcome::Denied,
ConfirmOutcome::TimedOut,
] {
let bio = MockBiometric::new(outcome);
let confirmer = BiometricConfirmer::with_biometric(bio);
let got = confirmer.confirm(&req(), Duration::from_secs(1));
assert_eq!(got, outcome);
assert_eq!(got.is_approved(), outcome == ConfirmOutcome::Approved);
}
}
#[test]
fn timeout_fails_safe_to_denial() {
let confirmer =
BiometricConfirmer::with_biometric(MockBiometric::new(ConfirmOutcome::TimedOut));
let got = confirmer.confirm(&req(), Duration::ZERO);
assert_eq!(got, ConfirmOutcome::TimedOut);
assert!(!got.is_approved());
}
#[test]
fn no_self_approve_resolution_only_via_biometric() {
let bio = MockBiometric::new(ConfirmOutcome::Denied);
let confirmer = BiometricConfirmer::with_biometric(bio);
let got = confirmer.confirm(&req(), Duration::from_secs(1));
assert_eq!(got, ConfirmOutcome::Denied);
assert_eq!(confirmer.biometric.prompted.get(), 1);
}
#[test]
fn i7_i12_no_secret_value_in_confirm_path() {
let bio = MockBiometric::new(ConfirmOutcome::Approved);
let confirmer = BiometricConfirmer::with_biometric(bio);
let _ = confirmer.confirm(&req(), Duration::from_secs(1));
let text = confirmer.biometric.last_text.borrow().clone().unwrap();
assert!(text.contains("Environment: prod"));
assert!(text.contains("Secret: db/password"));
assert!(text.contains("/usr/bin/deploy --env prod"));
assert!(!text.to_lowercase().contains("secret-value"));
}
}