use std::future::Future;
use std::pin::Pin;
use crate::policy::{Act, Rule};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
pub act: Act,
pub target: String,
pub content: Option<String>,
}
impl Request {
pub fn new(act: Act, target: impl Into<String>) -> Self {
Self {
act,
target: target.into(),
content: None,
}
}
pub fn with_content(mut self, content: impl Into<String>) -> Self {
self.content = Some(content.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Approve {
modified: Option<Request>,
remember: Vec<Rule>,
},
Deny {
reason: String,
},
Defer,
}
impl Decision {
pub fn approve() -> Self {
Decision::Approve {
modified: None,
remember: Vec::new(),
}
}
pub fn deny(reason: impl Into<String>) -> Self {
Decision::Deny {
reason: reason.into(),
}
}
}
pub type DecisionFuture<'a> = Pin<Box<dyn Future<Output = Decision> + Send + 'a>>;
pub trait Approver: Send + Sync {
fn decide<'a>(&'a self, request: &'a Request) -> DecisionFuture<'a>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ApproveAll;
impl Approver for ApproveAll {
fn decide<'a>(&'a self, _request: &'a Request) -> DecisionFuture<'a> {
Box::pin(async { Decision::approve() })
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DenyAll;
impl Approver for DenyAll {
fn decide<'a>(&'a self, _request: &'a Request) -> DecisionFuture<'a> {
Box::pin(async { Decision::deny("no approver available") })
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct StdinApprover;
impl Approver for StdinApprover {
fn decide<'a>(&'a self, request: &'a Request) -> DecisionFuture<'a> {
Box::pin(async move {
use std::io::Write;
let mut out = std::io::stdout();
let _ = write!(out, "\nallow {:?} {}? [y/N] ", request.act, request.target);
let _ = out.flush();
let mut line = String::new();
match std::io::stdin().read_line(&mut line) {
Ok(_) if line.trim().eq_ignore_ascii_case("y") => Decision::approve(),
_ => Decision::deny("denied at the terminal"),
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::sync::oneshot;
#[tokio::test]
async fn built_in_approvers_decide_as_advertised() {
let req = Request::new(Act::Write, "src/a.rs");
assert_eq!(ApproveAll.decide(&req).await, Decision::approve());
assert!(matches!(
DenyAll.decide(&req).await,
Decision::Deny { .. }
));
}
struct ChannelApprover {
answer: std::sync::Mutex<Option<oneshot::Receiver<Decision>>>,
}
impl Approver for ChannelApprover {
fn decide<'a>(&'a self, _request: &'a Request) -> DecisionFuture<'a> {
let rx = self.answer.lock().unwrap().take();
Box::pin(async move {
match rx {
Some(rx) => rx.await.unwrap_or(Decision::deny("channel closed")),
None => Decision::deny("already decided"),
}
})
}
}
#[tokio::test]
async fn an_approver_can_be_held_as_a_trait_object_and_answer_out_of_band() {
let (tx, rx) = oneshot::channel();
let approver: Box<dyn Approver> = Box::new(ChannelApprover {
answer: std::sync::Mutex::new(Some(rx)),
});
tokio::spawn(async move {
tokio::task::yield_now().await;
let _ = tx.send(Decision::approve());
});
let req = Request::new(Act::Write, "src/a.rs");
assert_eq!(approver.decide(&req).await, Decision::approve());
}
#[tokio::test]
async fn a_slow_approver_keeps_the_caller_waiting_rather_than_timing_out() {
struct Slow;
impl Approver for Slow {
fn decide<'a>(&'a self, _r: &'a Request) -> DecisionFuture<'a> {
Box::pin(async {
tokio::time::sleep(std::time::Duration::from_millis(60)).await;
Decision::approve()
})
}
}
let started = std::time::Instant::now();
let d = Slow.decide(&Request::new(Act::Write, "x")).await;
assert_eq!(d, Decision::approve());
assert!(started.elapsed() >= std::time::Duration::from_millis(50));
}
}