use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use super::policy::PolicyVersion;
use super::rules::RuleId;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ActionClass {
Read,
Write,
Exec,
Net,
Llm,
Custom,
}
impl ActionClass {
pub fn name(self) -> &'static str {
match self {
ActionClass::Read => "read",
ActionClass::Write => "write",
ActionClass::Exec => "exec",
ActionClass::Net => "net",
ActionClass::Llm => "llm",
ActionClass::Custom => "custom",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Risk {
Low,
Medium,
High,
Critical,
}
impl Risk {
pub fn default_for(class: ActionClass) -> Self {
match class {
ActionClass::Read => Risk::Low,
ActionClass::Net | ActionClass::Llm => Risk::Medium,
ActionClass::Write | ActionClass::Exec => Risk::High,
ActionClass::Custom => Risk::High,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DecisionScope {
Session,
Workspace,
User,
Always,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PermissionRequest {
pub id: String,
pub tenant_id: Option<String>,
pub session_id: String,
pub workspace_id: Option<String>,
pub actor: String,
pub class: ActionClass,
pub action: String,
pub target: String,
pub risk: Option<Risk>,
#[serde(default)]
pub context: BTreeMap<String, serde_json::Value>,
pub reason: Option<String>,
pub requested_at: OffsetDateTime,
}
impl PermissionRequest {
pub fn new(
id: impl Into<String>,
session_id: impl Into<String>,
actor: impl Into<String>,
class: ActionClass,
action: impl Into<String>,
target: impl Into<String>,
) -> Self {
Self {
id: id.into(),
tenant_id: None,
session_id: session_id.into(),
workspace_id: None,
actor: actor.into(),
class,
action: action.into(),
target: target.into(),
risk: None,
context: BTreeMap::new(),
reason: None,
requested_at: OffsetDateTime::now_utc(),
}
}
pub fn effective_risk(&self) -> Risk {
self.risk.unwrap_or_else(|| Risk::default_for(self.class))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum PermissionDecision {
Granted {
scope: DecisionScope,
policy_version: PolicyVersion,
reason: Option<String>,
expires_at: Option<OffsetDateTime>,
rule_id: Option<RuleId>,
},
Denied {
scope: DecisionScope,
policy_version: PolicyVersion,
reason: Option<String>,
rule_id: Option<RuleId>,
},
Suspend {
policy_version: PolicyVersion,
escalate_to: Vec<String>,
reason: Option<String>,
},
}
impl PermissionDecision {
pub fn is_granted(&self) -> bool {
matches!(self, PermissionDecision::Granted { .. })
}
pub fn policy_version(&self) -> &PolicyVersion {
match self {
PermissionDecision::Granted { policy_version, .. }
| PermissionDecision::Denied { policy_version, .. }
| PermissionDecision::Suspend { policy_version, .. } => policy_version,
}
}
pub fn reason(&self) -> Option<&str> {
match self {
PermissionDecision::Granted { reason, .. }
| PermissionDecision::Denied { reason, .. }
| PermissionDecision::Suspend { reason, .. } => reason.as_deref(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn risk_defaults_reflect_action_class() {
assert_eq!(Risk::default_for(ActionClass::Read), Risk::Low);
assert_eq!(Risk::default_for(ActionClass::Net), Risk::Medium);
assert_eq!(Risk::default_for(ActionClass::Llm), Risk::Medium);
assert_eq!(Risk::default_for(ActionClass::Write), Risk::High);
assert_eq!(Risk::default_for(ActionClass::Exec), Risk::High);
assert_eq!(Risk::default_for(ActionClass::Custom), Risk::High);
}
#[test]
fn effective_risk_prefers_override() {
let mut req =
PermissionRequest::new("p1", "s1", "alice", ActionClass::Read, "fs.read", "/tmp/a");
assert_eq!(req.effective_risk(), Risk::Low);
req.risk = Some(Risk::Critical);
assert_eq!(req.effective_risk(), Risk::Critical);
}
#[test]
fn decision_scope_orders_narrow_to_broad() {
assert!(DecisionScope::Session < DecisionScope::Workspace);
assert!(DecisionScope::Workspace < DecisionScope::User);
assert!(DecisionScope::User < DecisionScope::Always);
}
}