use async_trait::async_trait;
use origin_domain::{AppError, Result};
use std::fmt::Debug;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProcessOutput {
pub status: i32,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
impl ProcessOutput {
pub fn success(&self) -> bool {
self.status == 0
}
}
#[derive(Debug, Clone, Default)]
pub struct ProcessAllowlist {
programs: Vec<String>,
}
impl ProcessAllowlist {
pub fn new(programs: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
programs: programs.into_iter().map(Into::into).collect(),
}
}
pub fn allows(&self, program: &str) -> bool {
self.programs.iter().any(|p| p == program)
}
pub fn check(&self, program: &str) -> Result<()> {
if self.allows(program) {
Ok(())
} else {
Err(AppError::Permission(format!(
"program `{program}` is not in the process allowlist"
)))
}
}
pub fn entries(&self) -> &[String] {
&self.programs
}
}
#[async_trait]
pub trait ProcessRunner: Debug + Send + Sync + 'static {
async fn run(&self, program: &str, args: &[String], cwd: &Path) -> Result<ProcessOutput>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allowlist_rejects_an_unlisted_program_with_permission_error() {
let allowlist = ProcessAllowlist::new(["git"]);
let result = allowlist.check("rm");
match result {
Err(AppError::Permission(_)) => {} other => panic!("expected Permission error, got {other:?}"),
}
}
#[test]
fn allowlist_permits_a_listed_program() {
let allowlist = ProcessAllowlist::new(["git", "code"]);
assert!(allowlist.allows("git"));
assert!(allowlist.allows("code"));
allowlist
.check("git")
.expect("listed program must be allowed");
}
}