1use crate::SecurityRequestView;
18
19#[must_use = "authorization decisions must be checked; Ok(Deny(_)) rejects the operation"]
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum AuthorizationDecision {
23 Allow,
25 Deny(AuthorizationDenial),
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum AuthorizationDenial {
35 SubjectUnknown,
37 ResourceUnknown,
39 PermissionDenied,
41 PolicyNotApplicable,
43 MaintenanceRestricted,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum IngressDecision {
50 AllowToContinue,
52 Deny,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum DetailedDecision {
62 Allow,
64 Deny,
66 Abstain,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum LayerFailureKind {
73 Unavailable,
75 Error,
77 Timeout,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum LayerRequirement {
84 Required,
86 Optional,
91}
92
93pub type LayerEvaluation<T> = Result<T, LayerFailureKind>;
98
99pub trait IngressPolicy: Send + Sync {
104 fn evaluate_ingress(&self, request: SecurityRequestView<'_>) -> LayerEvaluation<IngressDecision>;
106}
107
108pub fn combine_layered_authorization<F>(
116 ingress: LayerEvaluation<IngressDecision>,
117 requirement: LayerRequirement,
118 detailed: F,
119) -> LayerEvaluation<AuthorizationDecision>
120where
121 F: FnOnce() -> LayerEvaluation<DetailedDecision>,
122{
123 match ingress {
124 Ok(IngressDecision::AllowToContinue) => match detailed() {
125 Ok(DetailedDecision::Allow) => Ok(AuthorizationDecision::Allow),
126 Ok(DetailedDecision::Deny) => Ok(AuthorizationDecision::Deny(AuthorizationDenial::PermissionDenied)),
127 Ok(DetailedDecision::Abstain) if requirement == LayerRequirement::Optional => {
128 Ok(AuthorizationDecision::Allow)
129 }
130 Ok(DetailedDecision::Abstain) => Ok(AuthorizationDecision::Deny(AuthorizationDenial::PolicyNotApplicable)),
131 Err(failure) => Err(failure),
132 },
133 Ok(IngressDecision::Deny) => Ok(AuthorizationDecision::Deny(AuthorizationDenial::PermissionDenied)),
134 Err(failure) => Err(failure),
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use std::cell::Cell;
141
142 use super::*;
143
144 fn is_allow(decision: LayerEvaluation<AuthorizationDecision>) -> bool {
145 matches!(decision, Ok(AuthorizationDecision::Allow))
146 }
147
148 #[test]
149 fn complete_truth_table_is_fail_closed() {
150 for ingress in [IngressDecision::AllowToContinue, IngressDecision::Deny] {
151 for requirement in [LayerRequirement::Required, LayerRequirement::Optional] {
152 for detailed in [
153 DetailedDecision::Allow,
154 DetailedDecision::Deny,
155 DetailedDecision::Abstain,
156 ] {
157 let detailed_called = Cell::new(false);
158 let result = combine_layered_authorization(Ok(ingress), requirement, || {
159 detailed_called.set(true);
160 Ok(detailed)
161 });
162 let expected_allow = matches!(
163 (ingress, requirement, detailed),
164 (IngressDecision::AllowToContinue, _, DetailedDecision::Allow)
165 | (
166 IngressDecision::AllowToContinue,
167 LayerRequirement::Optional,
168 DetailedDecision::Abstain
169 )
170 );
171
172 assert_eq!(is_allow(result), expected_allow);
173 assert_eq!(detailed_called.get(), ingress == IngressDecision::AllowToContinue);
174 }
175 }
176 }
177 }
178
179 #[test]
180 fn coarse_deny_is_sticky_and_skips_detailed_evaluation() {
181 let detailed_called = Cell::new(false);
182 let result = combine_layered_authorization(Ok(IngressDecision::Deny), LayerRequirement::Optional, || {
183 detailed_called.set(true);
184 Ok(DetailedDecision::Allow)
185 });
186
187 assert_eq!(
188 result,
189 Ok(AuthorizationDecision::Deny(AuthorizationDenial::PermissionDenied))
190 );
191 assert!(!detailed_called.get());
192 }
193
194 #[test]
195 fn every_layer_failure_is_preserved() {
196 for failure in [
197 LayerFailureKind::Unavailable,
198 LayerFailureKind::Error,
199 LayerFailureKind::Timeout,
200 ] {
201 assert_eq!(
202 combine_layered_authorization(Err(failure), LayerRequirement::Optional, || {
203 Ok(DetailedDecision::Allow)
204 }),
205 Err(failure)
206 );
207 assert_eq!(
208 combine_layered_authorization(
209 Ok(IngressDecision::AllowToContinue),
210 LayerRequirement::Optional,
211 || Err(failure),
212 ),
213 Err(failure)
214 );
215 }
216 }
217
218 #[test]
219 fn detailed_deny_and_required_abstention_have_closed_reasons() {
220 let denied =
221 combine_layered_authorization(Ok(IngressDecision::AllowToContinue), LayerRequirement::Required, || {
222 Ok(DetailedDecision::Deny)
223 });
224 let abstained =
225 combine_layered_authorization(Ok(IngressDecision::AllowToContinue), LayerRequirement::Required, || {
226 Ok(DetailedDecision::Abstain)
227 });
228
229 assert_eq!(
230 denied,
231 Ok(AuthorizationDecision::Deny(AuthorizationDenial::PermissionDenied))
232 );
233 assert_eq!(
234 abstained,
235 Ok(AuthorizationDecision::Deny(AuthorizationDenial::PolicyNotApplicable))
236 );
237 }
238
239 #[test]
240 fn every_denial_reason_is_closed_and_value_free() {
241 let reasons = [
242 AuthorizationDenial::SubjectUnknown,
243 AuthorizationDenial::ResourceUnknown,
244 AuthorizationDenial::PermissionDenied,
245 AuthorizationDenial::PolicyNotApplicable,
246 AuthorizationDenial::MaintenanceRestricted,
247 ];
248
249 for reason in reasons {
250 let decision = AuthorizationDecision::Deny(reason);
251 assert_eq!(decision, AuthorizationDecision::Deny(reason));
252 }
253 }
254}