use ferrin_core::generate_text::ApprovalStatus;
use ferrin_spec::JsonValue;
use serde::Deserialize;
use serde::Serialize;
pub const UNRECOGNIZED_DECISION: &str = "unrecognized OPA policy decision";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "decision", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum PolicyDecision {
Allow {
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
Deny {
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
RequiresApproval {
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
NotApplicable,
}
impl PolicyDecision {
#[must_use]
pub fn allow() -> Self {
Self::Allow { reason: None }
}
#[must_use]
pub fn deny() -> Self {
Self::Deny { reason: None }
}
#[must_use]
pub fn requires_approval() -> Self {
Self::RequiresApproval { reason: None }
}
#[must_use]
pub fn with_reason(self, reason: impl Into<String>) -> Self {
let reason = Some(reason.into());
match self {
Self::NotApplicable => Self::NotApplicable,
Self::Allow { .. } => Self::Allow { reason },
Self::Deny { .. } => Self::Deny { reason },
Self::RequiresApproval { .. } => Self::RequiresApproval { reason },
}
}
#[must_use]
pub fn reason(&self) -> Option<&str> {
match self {
Self::NotApplicable => None,
Self::Allow { reason } | Self::Deny { reason } | Self::RequiresApproval { reason } => {
reason.as_deref()
}
}
}
#[must_use]
pub fn normalize(raw: &JsonValue) -> Self {
let unrecognized = || Self::Deny {
reason: Some(UNRECOGNIZED_DECISION.to_owned()),
};
match raw {
JsonValue::Null => Self::NotApplicable,
JsonValue::Object(object) => {
let reason = object
.get("reason")
.and_then(JsonValue::as_str)
.filter(|reason| !reason.is_empty())
.map(str::to_owned);
match object.get("decision").and_then(JsonValue::as_str) {
Some("allow") => return Self::Allow { reason },
Some("deny") => return Self::Deny { reason },
Some("requires-approval") => return Self::RequiresApproval { reason },
Some("not-applicable") => return Self::NotApplicable,
_ => {}
}
match object.get("allow").and_then(JsonValue::as_bool) {
Some(true) => Self::Allow { reason },
Some(false) => Self::Deny { reason },
None => unrecognized(),
}
}
_ => unrecognized(),
}
}
#[must_use]
pub fn into_approval(self) -> Option<ApprovalStatus> {
match self {
Self::Allow { reason } => Some(ApprovalStatus::Approved { reason }),
Self::Deny { reason } => Some(ApprovalStatus::Denied { reason }),
Self::RequiresApproval { reason } => Some(ApprovalStatus::UserApproval { reason }),
Self::NotApplicable => Some(ApprovalStatus::NotApplicable),
}
}
}