use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Scope {
Global,
Venture(String),
Subject(String),
}
impl Scope {
#[must_use]
pub fn key(&self) -> String {
match self {
Scope::Global => "global".to_owned(),
Scope::Venture(v) => format!("venture:{v}"),
Scope::Subject(s) => format!("subject:{s}"),
}
}
#[must_use]
pub fn for_actor(venture: &str, subject: &str) -> [Scope; 3] {
[
Scope::Global,
Scope::Venture(venture.to_owned()),
Scope::Subject(subject.to_owned()),
]
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("kill switch: {0}")]
pub struct KillSwitchError(pub String);
#[async_trait]
pub trait KillSwitch: Send + Sync {
async fn engaged(&self, scope: &Scope) -> Result<bool, KillSwitchError>;
async fn engage(&self, scope: Scope, reason: &str) -> Result<(), KillSwitchError>;
async fn release(&self, scope: &Scope) -> Result<(), KillSwitchError>;
}
#[must_use]
pub fn turnkey_deny_policy(scope: &Scope) -> Option<serde_json::Value> {
let plain = |id: &str| {
!id.is_empty()
&& id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b':' | b'-'))
};
let condition = match scope {
Scope::Global => "true".to_owned(),
Scope::Venture(v) if plain(v) => format!(r#"TRACING_TAGS.venture == "{v}""#),
Scope::Subject(s) if plain(s) => format!(r#"TRACING_TAGS.subject == "{s}""#),
_ => return None,
};
Some(serde_json::json!({ "effect": "EFFECT_DENY", "condition": condition }))
}