ferrin_policy/
decision.rs1use ferrin_core::generate_text::ApprovalStatus;
7use ferrin_spec::JsonValue;
8use serde::Deserialize;
9use serde::Serialize;
10
11pub const UNRECOGNIZED_DECISION: &str = "unrecognized OPA policy decision";
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(tag = "decision", rename_all = "kebab-case")]
17#[non_exhaustive]
18pub enum PolicyDecision {
19 Allow {
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 reason: Option<String>,
24 },
25 Deny {
27 #[serde(default, skip_serializing_if = "Option::is_none")]
29 reason: Option<String>,
30 },
31 RequiresApproval {
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 reason: Option<String>,
36 },
37 NotApplicable,
39}
40
41impl PolicyDecision {
42 #[must_use]
44 pub fn allow() -> Self {
45 Self::Allow { reason: None }
46 }
47
48 #[must_use]
50 pub fn deny() -> Self {
51 Self::Deny { reason: None }
52 }
53
54 #[must_use]
56 pub fn requires_approval() -> Self {
57 Self::RequiresApproval { reason: None }
58 }
59
60 #[must_use]
62 pub fn with_reason(self, reason: impl Into<String>) -> Self {
63 let reason = Some(reason.into());
64 match self {
65 Self::NotApplicable => Self::NotApplicable,
66 Self::Allow { .. } => Self::Allow { reason },
67 Self::Deny { .. } => Self::Deny { reason },
68 Self::RequiresApproval { .. } => Self::RequiresApproval { reason },
69 }
70 }
71
72 #[must_use]
74 pub fn reason(&self) -> Option<&str> {
75 match self {
76 Self::NotApplicable => None,
77 Self::Allow { reason } | Self::Deny { reason } | Self::RequiresApproval { reason } => {
78 reason.as_deref()
79 }
80 }
81 }
82
83 #[must_use]
95 pub fn normalize(raw: &JsonValue) -> Self {
96 let unrecognized = || Self::Deny {
97 reason: Some(UNRECOGNIZED_DECISION.to_owned()),
98 };
99 match raw {
100 JsonValue::Null => Self::NotApplicable,
101 JsonValue::Object(object) => {
102 let reason = object
103 .get("reason")
104 .and_then(JsonValue::as_str)
105 .filter(|reason| !reason.is_empty())
106 .map(str::to_owned);
107 match object.get("decision").and_then(JsonValue::as_str) {
108 Some("allow") => return Self::Allow { reason },
109 Some("deny") => return Self::Deny { reason },
110 Some("requires-approval") => return Self::RequiresApproval { reason },
111 Some("not-applicable") => return Self::NotApplicable,
112 _ => {}
113 }
114 match object.get("allow").and_then(JsonValue::as_bool) {
115 Some(true) => Self::Allow { reason },
116 Some(false) => Self::Deny { reason },
117 None => unrecognized(),
118 }
119 }
120 _ => unrecognized(),
121 }
122 }
123
124 #[must_use]
129 pub fn into_approval(self) -> Option<ApprovalStatus> {
130 match self {
131 Self::Allow { reason } => Some(ApprovalStatus::Approved { reason }),
132 Self::Deny { reason } => Some(ApprovalStatus::Denied { reason }),
133 Self::RequiresApproval { reason } => Some(ApprovalStatus::UserApproval { reason }),
134 Self::NotApplicable => Some(ApprovalStatus::NotApplicable),
135 }
136 }
137}