use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Mode {
Shadow,
Enforce,
}
pub struct Resp {
pub status: u16,
pub body: String,
}
pub trait Transport {
fn post(&self, url: &str, body: &str, idempotency_key: &str) -> Result<Resp, ()>;
}
pub struct Config {
pub api_key: Option<String>,
pub endpoint: Option<String>,
pub namespace: String,
pub mode: Mode,
pub retries: u32,
pub backoff_ms: u64,
}
impl Default for Config {
fn default() -> Self {
Config {
api_key: None,
endpoint: std::env::var("ADMIT_ENDPOINT").ok(),
namespace: "default".to_string(),
mode: Mode::Shadow,
retries: 3,
backoff_ms: 200,
}
}
}
#[derive(Clone, Debug)]
pub struct Decision {
pub decision: String,
pub reason_code: Option<String>,
pub token: Option<String>,
pub mode: Mode,
pub would_block: bool,
pub awaiting: bool,
}
impl Decision {
pub fn accepted(&self) -> bool {
self.decision == "ACCEPT"
}
pub fn refused(&self) -> bool {
self.decision == "REFUSE"
}
pub fn blocked(&self) -> bool {
self.mode == Mode::Enforce && self.would_block
}
}
#[derive(Debug)]
pub enum AdmitError {
Refused {
action: String,
reason_code: Option<String>,
},
Ungoverned,
}
struct Rec {
action: String,
would_block: bool,
reason_code: Option<String>,
awaiting: bool,
}
pub struct ShadowReport {
pub total: usize,
pub would_block_count: usize,
pub awaiting: usize,
pub would_have_blocked: Vec<(String, Option<String>)>,
pub summary: String,
}
pub struct Client<T: Transport> {
cfg: Config,
transport: T,
log: Vec<Rec>,
}
static COUNTER: AtomicU64 = AtomicU64::new(0);
impl<T: Transport> Client<T> {
pub fn new(mut cfg: Config, transport: T) -> Self {
if let Some(e) = cfg.endpoint.take() {
cfg.endpoint = Some(e.trim_end_matches('/').to_string());
}
Client {
cfg,
transport,
log: Vec::new(),
}
}
pub fn admit(&mut self, action: &str, metadata_json: &str) -> Result<Decision, AdmitError> {
let md = if metadata_json.trim().is_empty() {
"{}"
} else {
metadata_json
};
let body = format!(
"{{\"action\":{},\"namespace\":{},\"metadata\":{}}}",
json_str(action),
json_str(&self.cfg.namespace),
md
);
let has_ep = self
.cfg
.endpoint
.as_deref()
.map(|e| !e.is_empty())
.unwrap_or(false);
let raw = if has_ep {
let ep = self.cfg.endpoint.clone().unwrap();
self.call(&ep, &body)
} else {
Raw {
decision: "AWAITING".to_string(),
reason_code: Some("NO_ENDPOINT".to_string()),
token: None,
}
};
let would_block = raw.decision == "REFUSE";
let awaiting = raw.decision == "AWAITING";
self.log.push(Rec {
action: action.to_string(),
would_block,
reason_code: raw.reason_code.clone(),
awaiting,
});
let d = Decision {
decision: raw.decision.clone(),
reason_code: raw.reason_code.clone(),
token: raw.token.clone(),
mode: self.cfg.mode,
would_block,
awaiting,
};
if self.cfg.mode == Mode::Enforce {
if would_block {
return Err(AdmitError::Refused {
action: action.to_string(),
reason_code: raw.reason_code,
});
}
if awaiting {
return Err(AdmitError::Ungoverned);
}
}
Ok(d)
}
pub fn shadow_report(&self) -> ShadowReport {
let total = self.log.len();
let awaiting = self.log.iter().filter(|r| r.awaiting).count();
let would_have_blocked: Vec<(String, Option<String>)> = self
.log
.iter()
.filter(|r| r.would_block)
.map(|r| (r.action.clone(), r.reason_code.clone()))
.collect();
let summary = if awaiting == total && total > 0 {
format!(
"{} action(s) observed — /admit endpoint not wired yet; set ADMIT_ENDPOINT to see what would be blocked",
total
)
} else {
format!(
"{} of {} action(s) would have been blocked by /admit",
would_have_blocked.len(),
total
)
};
ShadowReport {
total,
would_block_count: would_have_blocked.len(),
awaiting,
would_have_blocked,
summary,
}
}
fn call(&self, endpoint: &str, body: &str) -> Raw {
let url = format!("{}/admit", endpoint);
let idem = idempotency_key();
let mut attempt: u32 = 0;
loop {
if attempt > 0 {
let ms = self.cfg.backoff_ms.saturating_mul(1u64 << (attempt - 1));
std::thread::sleep(std::time::Duration::from_millis(ms));
}
match self.transport.post(&url, body, &idem) {
Ok(resp) => {
if resp.status == 502 || resp.status == 503 || resp.status == 504 {
if attempt < self.cfg.retries {
attempt += 1;
continue;
}
return unreachable_raw();
}
let mut decision = extract(&resp.body, "decision").unwrap_or_default();
let mut reason_code = extract(&resp.body, "reason_code");
if decision.is_empty() {
decision = if (200..300).contains(&resp.status) {
"ACCEPT".to_string()
} else {
if reason_code.is_none() {
reason_code = Some(format!("HTTP_{}", resp.status));
}
"AWAITING".to_string()
};
}
if decision == "ACCEPT_ESCROW" {
decision = "ACCEPT".to_string();
}
return Raw {
decision: decision.to_uppercase(),
reason_code,
token: extract(&resp.body, "token"),
};
}
Err(_) => {
if attempt < self.cfg.retries {
attempt += 1;
continue;
}
return unreachable_raw();
}
}
}
}
}
struct Raw {
decision: String,
reason_code: Option<String>,
token: Option<String>,
}
fn unreachable_raw() -> Raw {
Raw {
decision: "AWAITING".to_string(),
reason_code: Some("UNREACHABLE".to_string()),
token: None,
}
}
fn json_str(s: &str) -> String {
let esc = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{}\"", esc)
}
fn extract(json: &str, key: &str) -> Option<String> {
let pat = format!("\"{}\"", key);
let i = json.find(&pat)? + pat.len();
let rest = &json[i..];
let colon = rest.find(':')?;
let after = rest[colon + 1..].trim_start();
let after = after.strip_prefix('"')?;
let end = after.find('"')?;
Some(after[..end].to_string())
}
fn idempotency_key() -> String {
let n = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let c = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("idem_{:x}_{:x}", n, c)
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::rc::Rc;
struct Fake;
impl Transport for Fake {
fn post(&self, _url: &str, body: &str, _idem: &str) -> Result<Resp, ()> {
if body.contains("prod") {
Ok(Resp {
status: 403,
body: "{\"decision\":\"REFUSE\",\"reason_code\":\"POLICY_DENIED\"}".to_string(),
})
} else {
Ok(Resp {
status: 200,
body: "{\"decision\":\"ACCEPT\",\"token\":\"t\"}".to_string(),
})
}
}
}
fn client(mode: Mode) -> Client<Fake> {
Client::new(
Config {
endpoint: Some("https://x".to_string()),
mode,
..Default::default()
},
Fake,
)
}
#[test]
fn shadow_never_blocks_even_on_refuse() {
let mut c = client(Mode::Shadow);
let d = c.admit("deploy_prod", "{}").unwrap();
assert!(d.refused() && d.would_block && !d.blocked());
}
#[test]
fn the_shadow_report() {
let mut c = client(Mode::Shadow);
let _ = c.admit("read_config", "{}");
let _ = c.admit("deploy_prod", "{}");
let _ = c.admit("delete_prod_db", "{}");
let r = c.shadow_report();
assert_eq!(r.total, 3);
assert_eq!(r.would_block_count, 2);
assert_eq!(
r.summary,
"2 of 3 action(s) would have been blocked by /admit"
);
}
#[test]
fn enforce_refusal_is_an_error() {
let mut c = client(Mode::Enforce);
match c.admit("deploy_prod", "{}") {
Err(AdmitError::Refused { .. }) => {}
other => panic!("want Refused, got {:?}", other),
}
assert!(c.admit("read_config", "{}").unwrap().accepted());
}
#[test]
fn no_endpoint_is_honest_awaiting() {
let mut c = Client::new(
Config {
endpoint: None,
..Default::default()
},
Fake,
);
let d = c.admit("deploy_prod", "{}").unwrap();
assert!(d.awaiting && !d.would_block);
}
struct Flaky {
calls: Rc<Cell<u32>>,
keys: Rc<RefCell<HashSet<String>>>,
}
impl Transport for Flaky {
fn post(&self, _url: &str, _body: &str, idem: &str) -> Result<Resp, ()> {
self.keys.borrow_mut().insert(idem.to_string());
let c = self.calls.get() + 1;
self.calls.set(c);
if c < 3 {
Ok(Resp {
status: 503,
body: String::new(),
})
} else {
Ok(Resp {
status: 200,
body: "{\"decision\":\"ACCEPT\"}".to_string(),
})
}
}
}
#[test]
fn retries_transient_503_under_one_idempotency_key() {
let calls = Rc::new(Cell::new(0u32));
let keys = Rc::new(RefCell::new(HashSet::new()));
let f = Flaky {
calls: calls.clone(),
keys: keys.clone(),
};
let mut c = Client::new(
Config {
endpoint: Some("https://x".to_string()),
backoff_ms: 1,
retries: 3,
..Default::default()
},
f,
);
let d = c.admit("read_config", "{}").unwrap();
assert!(d.accepted());
assert_eq!(calls.get(), 3);
assert_eq!(keys.borrow().len(), 1); }
}