Skip to main content

chio_openapi/
policy.rs

1//! Default policy assignment for OpenAPI operations.
2//!
3//! Safe HTTP methods (GET, HEAD, OPTIONS) receive session-scoped allow.
4//! Side-effect methods (POST, PUT, PATCH, DELETE) are deny-by-default and
5//! require an explicit capability grant.
6
7use chio_http_core::HttpMethod;
8
9use crate::extensions::ChioExtensions;
10
11/// The policy decision for a given operation.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum PolicyDecision {
14    /// Session-scoped allow -- the operation is permitted by default within
15    /// an active session.
16    SessionAllow,
17    /// Deny by default -- the operation requires an explicit capability grant.
18    DenyByDefault,
19}
20
21/// Computes the default policy for an operation given its HTTP method and
22/// any Chio extension overrides.
23pub struct DefaultPolicy;
24
25impl DefaultPolicy {
26    /// Determine the policy decision for an HTTP method. Safe methods get
27    /// session-scoped allow; side-effect methods get deny-by-default.
28    #[must_use]
29    pub fn for_method(method: HttpMethod) -> PolicyDecision {
30        if method.is_safe() {
31            PolicyDecision::SessionAllow
32        } else {
33            PolicyDecision::DenyByDefault
34        }
35    }
36
37    /// Determine the policy decision, taking Chio extensions into account.
38    ///
39    /// If `x-chio-side-effects` is explicitly set, it overrides the method
40    /// default: `true` forces deny-by-default, `false` forces session-allow.
41    /// If `x-chio-approval-required` is `true`, the result is always
42    /// deny-by-default regardless of other settings.
43    #[must_use]
44    pub fn for_method_with_extensions(
45        method: HttpMethod,
46        extensions: &ChioExtensions,
47    ) -> PolicyDecision {
48        // Approval-required always forces deny.
49        if extensions.approval_required == Some(true) {
50            return PolicyDecision::DenyByDefault;
51        }
52
53        // Explicit side-effects override takes priority over method default.
54        if let Some(has_side_effects) = extensions.side_effects {
55            return if has_side_effects {
56                PolicyDecision::DenyByDefault
57            } else {
58                PolicyDecision::SessionAllow
59            };
60        }
61
62        Self::for_method(method)
63    }
64
65    /// Whether the operation has side effects, considering both the HTTP method
66    /// default and any Chio extension override.
67    #[must_use]
68    pub fn has_side_effects(method: HttpMethod, extensions: &ChioExtensions) -> bool {
69        if let Some(explicit) = extensions.side_effects {
70            return explicit;
71        }
72        method.requires_capability()
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn safe_methods_allow() {
82        assert_eq!(
83            DefaultPolicy::for_method(HttpMethod::Get),
84            PolicyDecision::SessionAllow
85        );
86        assert_eq!(
87            DefaultPolicy::for_method(HttpMethod::Head),
88            PolicyDecision::SessionAllow
89        );
90        assert_eq!(
91            DefaultPolicy::for_method(HttpMethod::Options),
92            PolicyDecision::SessionAllow
93        );
94    }
95
96    #[test]
97    fn unsafe_methods_deny() {
98        assert_eq!(
99            DefaultPolicy::for_method(HttpMethod::Post),
100            PolicyDecision::DenyByDefault
101        );
102        assert_eq!(
103            DefaultPolicy::for_method(HttpMethod::Put),
104            PolicyDecision::DenyByDefault
105        );
106        assert_eq!(
107            DefaultPolicy::for_method(HttpMethod::Patch),
108            PolicyDecision::DenyByDefault
109        );
110        assert_eq!(
111            DefaultPolicy::for_method(HttpMethod::Delete),
112            PolicyDecision::DenyByDefault
113        );
114    }
115
116    #[test]
117    fn extension_side_effects_override() {
118        let mut ext = ChioExtensions {
119            side_effects: Some(true),
120            ..Default::default()
121        };
122        assert_eq!(
123            DefaultPolicy::for_method_with_extensions(HttpMethod::Get, &ext),
124            PolicyDecision::DenyByDefault
125        );
126
127        ext.side_effects = Some(false);
128        assert_eq!(
129            DefaultPolicy::for_method_with_extensions(HttpMethod::Post, &ext),
130            PolicyDecision::SessionAllow
131        );
132    }
133
134    #[test]
135    fn approval_required_always_denies() {
136        let ext = ChioExtensions {
137            approval_required: Some(true),
138            side_effects: Some(false),
139            ..Default::default()
140        };
141
142        assert_eq!(
143            DefaultPolicy::for_method_with_extensions(HttpMethod::Get, &ext),
144            PolicyDecision::DenyByDefault
145        );
146    }
147
148    #[test]
149    fn has_side_effects_follows_method() {
150        let ext = ChioExtensions::default();
151        assert!(!DefaultPolicy::has_side_effects(HttpMethod::Get, &ext));
152        assert!(DefaultPolicy::has_side_effects(HttpMethod::Post, &ext));
153    }
154
155    #[test]
156    fn has_side_effects_respects_override() {
157        let mut ext = ChioExtensions {
158            side_effects: Some(true),
159            ..Default::default()
160        };
161        assert!(DefaultPolicy::has_side_effects(HttpMethod::Get, &ext));
162
163        ext.side_effects = Some(false);
164        assert!(!DefaultPolicy::has_side_effects(HttpMethod::Delete, &ext));
165    }
166}