1use crate::ast::*;
24use crate::entities::Entities;
25use crate::evaluator::Evaluator;
26use crate::extensions::Extensions;
27use itertools::{Either, Itertools};
28use serde::{Deserialize, Serialize};
29use std::collections::HashSet;
30use std::sync::Arc;
31
32#[cfg(feature = "wasm")]
33extern crate tsify;
34
35mod err;
36mod partial_response;
37pub use err::{AuthorizationError, ConcretizationError, ReauthorizationError};
38
39pub use partial_response::ErrorState;
40pub use partial_response::PartialResponse;
41
42#[derive(Clone)] pub struct Authorizer {
45 extensions: &'static Extensions<'static>,
47 error_handling: ErrorHandling,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56enum ErrorHandling {
57 Skip,
60}
61
62impl Default for ErrorHandling {
63 fn default() -> Self {
64 Self::Skip
65 }
66}
67
68impl Authorizer {
69 pub fn new() -> Self {
71 Self {
72 extensions: Extensions::all_available(), error_handling: Default::default(),
74 }
75 }
76
77 pub fn is_authorized(&self, q: Request, pset: &PolicySet, entities: &Entities) -> Response {
82 self.is_authorized_core(q, pset, entities).concretize()
83 }
84
85 pub fn is_authorized_core(
89 &self,
90 q: Request,
91 pset: &PolicySet,
92 entities: &Entities,
93 ) -> PartialResponse {
94 let eval = Evaluator::new(q.clone(), entities, self.extensions);
95 let mut true_permits = vec![];
96 let mut true_forbids = vec![];
97 let mut false_permits = vec![];
98 let mut false_forbids = vec![];
99 let mut residual_permits = vec![];
100 let mut residual_forbids = vec![];
101 let mut errors = vec![];
102
103 for p in pset.policies() {
104 let (id, annotations) = (p.id().clone(), p.annotations_arc().clone());
105 match eval.partial_evaluate(p) {
106 Ok(Either::Left(satisfied)) => match (satisfied, p.effect()) {
107 (true, Effect::Permit) => true_permits.push((id, annotations)),
108 (true, Effect::Forbid) => true_forbids.push((id, annotations)),
109 (false, Effect::Permit) => {
110 false_permits.push((id, (ErrorState::NoError, annotations)))
111 }
112 (false, Effect::Forbid) => {
113 false_forbids.push((id, (ErrorState::NoError, annotations)))
114 }
115 },
116 Ok(Either::Right(residual)) => match p.effect() {
117 Effect::Permit => {
118 residual_permits.push((id, (Arc::new(residual), annotations)))
119 }
120 Effect::Forbid => {
121 residual_forbids.push((id, (Arc::new(residual), annotations)))
122 }
123 },
124 Err(e) => {
125 errors.push(AuthorizationError::PolicyEvaluationError {
126 id: id.clone(),
127 error: e,
128 });
129 let satisfied = match self.error_handling {
130 ErrorHandling::Skip => false,
131 };
132 match (satisfied, p.effect()) {
133 (true, Effect::Permit) => true_permits.push((id, annotations)),
134 (true, Effect::Forbid) => true_forbids.push((id, annotations)),
135 (false, Effect::Permit) => {
136 false_permits.push((id, (ErrorState::Error, annotations)))
137 }
138 (false, Effect::Forbid) => {
139 false_forbids.push((id, (ErrorState::Error, annotations)))
140 }
141 }
142 }
143 };
144 }
145
146 PartialResponse::new(
147 true_permits,
148 false_permits,
149 residual_permits,
150 true_forbids,
151 false_forbids,
152 residual_forbids,
153 errors,
154 Arc::new(q),
155 )
156 }
157}
158
159impl Default for Authorizer {
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165impl std::fmt::Debug for Authorizer {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 if self.extensions.ext_names().next().is_none() {
168 write!(f, "<Authorizer with no extensions>")
169 } else {
170 write!(
171 f,
172 "<Authorizer with the following extensions: [{}]>",
173 self.extensions.ext_names().join(", ")
174 )
175 }
176 }
177}
178
179#[allow(clippy::panic)]
181#[cfg(test)]
182mod test {
183 use crate::ast::Annotations;
184
185 use super::*;
186 use crate::parser;
187
188 #[test]
191 fn authorizer_sanity_check_empty() {
192 let a = Authorizer::new();
193 let q = Request::new(
194 (EntityUID::with_eid("p"), None),
195 (EntityUID::with_eid("a"), None),
196 (EntityUID::with_eid("r"), None),
197 Context::empty(),
198 None::<&RequestSchemaAllPass>,
199 Extensions::none(),
200 )
201 .unwrap();
202 let pset = PolicySet::new();
203 let entities = Entities::new();
204 let ans = a.is_authorized(q, &pset, &entities);
205 assert_eq!(ans.decision, Decision::Deny);
206 }
207
208 #[test]
210 fn skip_on_error_tests() {
211 let a = Authorizer::new();
212 let q = Request::new(
213 (EntityUID::with_eid("p"), None),
214 (EntityUID::with_eid("a"), None),
215 (EntityUID::with_eid("r"), None),
216 Context::empty(),
217 None::<&RequestSchemaAllPass>,
218 Extensions::none(),
219 )
220 .unwrap();
221 let mut pset = PolicySet::new();
222 let entities = Entities::new();
223
224 let p1_src = r#"
225 permit(principal, action, resource);
226 "#;
227
228 let p2_src = r#"
229 permit(principal, action, resource) when { context.bad == 2 };
230 "#;
231
232 let p3_src = r#"
233 forbid(principal, action, resource) when { context.bad == 2 };
234 "#;
235 let p4_src = r#"
236 forbid(principal, action, resource);
237 "#;
238
239 let p1 = parser::parse_policy(Some(PolicyID::from_string("1")), p1_src).unwrap();
240 pset.add_static(p1).unwrap();
241
242 let ans = a.is_authorized(q.clone(), &pset, &entities);
243 assert_eq!(ans.decision, Decision::Allow);
244
245 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("2")), p2_src).unwrap())
246 .unwrap();
247
248 let ans = a.is_authorized(q.clone(), &pset, &entities);
249 assert_eq!(ans.decision, Decision::Allow);
250
251 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("3")), p3_src).unwrap())
252 .unwrap();
253
254 let ans = a.is_authorized(q.clone(), &pset, &entities);
255 assert_eq!(ans.decision, Decision::Allow);
256
257 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("4")), p4_src).unwrap())
258 .unwrap();
259
260 let ans = a.is_authorized(q, &pset, &entities);
261 assert_eq!(ans.decision, Decision::Deny);
262 }
263
264 fn true_policy(id: &str, e: Effect) -> StaticPolicy {
265 let pid = PolicyID::from_string(id);
266 StaticPolicy::new(
267 pid,
268 None,
269 Annotations::new(),
270 e,
271 PrincipalConstraint::any(),
272 ActionConstraint::any(),
273 ResourceConstraint::any(),
274 Expr::val(true),
275 )
276 .expect("Policy Creation Failed")
277 }
278
279 #[cfg(feature = "partial-eval")]
280 fn context_pol(id: &str, effect: Effect) -> StaticPolicy {
281 let pid = PolicyID::from_string(id);
282 StaticPolicy::new(
283 pid,
284 None,
285 Annotations::new(),
286 effect,
287 PrincipalConstraint::any(),
288 ActionConstraint::any(),
289 ResourceConstraint::any(),
290 Expr::get_attr(Expr::var(Var::Context), "test".into()),
291 )
292 .expect("Policy Creation Failed")
293 }
294
295 #[test]
296 fn authorizer_sanity_check_allow() {
297 let a = Authorizer::new();
298 let q = Request::new(
299 (EntityUID::with_eid("p"), None),
300 (EntityUID::with_eid("a"), None),
301 (EntityUID::with_eid("r"), None),
302 Context::empty(),
303 None::<&RequestSchemaAllPass>,
304 Extensions::none(),
305 )
306 .unwrap();
307 let mut pset = PolicySet::new();
308 pset.add_static(true_policy("0", Effect::Permit))
309 .expect("Policy ID already in PolicySet");
310 let entities = Entities::new();
311 let ans = a.is_authorized(q, &pset, &entities);
312 assert!(ans.decision == Decision::Allow);
313 }
314
315 #[test]
316 #[cfg(feature = "partial-eval")]
317 fn authorizer_sanity_check_partial_deny() {
318 let context = Context::from_expr(
319 RestrictedExpr::record([(
320 "test".into(),
321 RestrictedExpr::unknown(Unknown::new_untyped("name")),
322 )])
323 .unwrap()
324 .as_borrowed(),
325 Extensions::none(),
326 )
327 .unwrap();
328 let a = Authorizer::new();
329 let q = Request::new(
330 (EntityUID::with_eid("p"), None),
331 (EntityUID::with_eid("a"), None),
332 (EntityUID::with_eid("r"), None),
333 context,
334 None::<&RequestSchemaAllPass>,
335 Extensions::none(),
336 )
337 .unwrap();
338 let mut pset = PolicySet::new();
339 pset.add_static(true_policy("0", Effect::Permit))
340 .expect("Policy ID already in PolicySet");
341 let entities = Entities::new();
342 let ans = a.is_authorized(q.clone(), &pset, &entities);
343 assert_eq!(ans.decision, Decision::Allow);
344 pset.add_static(context_pol("1", Effect::Forbid))
345 .expect("Policy ID overlap");
346 let ans = a.is_authorized(q.clone(), &pset, &entities);
347 assert_eq!(ans.decision, Decision::Allow);
348
349 let mut pset = PolicySet::new();
350 let entities = Entities::new();
351 pset.add_static(context_pol("1", Effect::Forbid))
352 .expect("Policy ID overlap");
353 let ans = a.is_authorized(q.clone(), &pset, &entities);
354 assert_eq!(ans.decision, Decision::Deny);
355
356 let mut pset = PolicySet::new();
357 let entities = Entities::new();
358 pset.add_static(context_pol("1", Effect::Permit))
359 .expect("Policy ID overlap");
360 let ans = a.is_authorized(q, &pset, &entities);
361 assert_eq!(ans.decision, Decision::Deny);
362 }
363
364 #[test]
365 fn authorizer_sanity_check_deny() {
366 let a = Authorizer::new();
367 let q = Request::new(
368 (EntityUID::with_eid("p"), None),
369 (EntityUID::with_eid("a"), None),
370 (EntityUID::with_eid("r"), None),
371 Context::empty(),
372 None::<&RequestSchemaAllPass>,
373 Extensions::none(),
374 )
375 .unwrap();
376 let mut pset = PolicySet::new();
377 pset.add_static(true_policy("0", Effect::Permit))
378 .expect("Policy ID already in PolicySet");
379 pset.add_static(true_policy("1", Effect::Forbid))
380 .expect("Policy ID already in PolicySet");
381 let entities = Entities::new();
382 let ans = a.is_authorized(q, &pset, &entities);
383 assert!(ans.decision == Decision::Deny);
384 }
385
386 #[test]
387 fn satisfied_permit_no_forbids() {
388 let q = Request::new(
389 (EntityUID::with_eid("p"), None),
390 (EntityUID::with_eid("a"), None),
391 (EntityUID::with_eid("r"), None),
392 Context::empty(),
393 None::<&RequestSchemaAllPass>,
394 Extensions::none(),
395 )
396 .unwrap();
397 let a = Authorizer::new();
398 let mut pset = PolicySet::new();
399 let es = Entities::new();
400
401 let src1 = r#"
402 permit(principal == test_entity_type::"p",action,resource);
403 "#;
404 let src2 = r#"
405 forbid(principal == test_entity_type::"p",action,resource) when {
406 false
407 };
408 "#;
409
410 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("1")), src1).unwrap())
411 .unwrap();
412 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("2")), src2).unwrap())
413 .unwrap();
414
415 let r = a.is_authorized_core(q, &pset, &es).decision();
416 assert_eq!(r, Some(Decision::Allow));
417 }
418
419 #[test]
420 #[cfg(feature = "partial-eval")]
421 fn satisfied_permit_no_forbids_unknown() {
422 let q = Request::new(
423 (EntityUID::with_eid("p"), None),
424 (EntityUID::with_eid("a"), None),
425 (EntityUID::with_eid("r"), None),
426 Context::empty(),
427 None::<&RequestSchemaAllPass>,
428 Extensions::none(),
429 )
430 .unwrap();
431 let a = Authorizer::new();
432 let mut pset = PolicySet::new();
433 let es = Entities::new();
434
435 let src1 = r#"
436 permit(principal == test_entity_type::"p",action,resource);
437 "#;
438 let src2 = r#"
439 forbid(principal == test_entity_type::"p",action,resource) when {
440 false
441 };
442 "#;
443 let src3 = r#"
444 permit(principal == test_entity_type::"p",action,resource) when {
445 unknown("test")
446 };
447 "#;
448
449 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("1")), src1).unwrap())
450 .unwrap();
451 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("2")), src2).unwrap())
452 .unwrap();
453
454 let r = a.is_authorized_core(q.clone(), &pset, &es).decision();
455 assert_eq!(r, Some(Decision::Allow));
456
457 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("3")), src3).unwrap())
458 .unwrap();
459
460 let r = a.is_authorized_core(q.clone(), &pset, &es).decision();
461 assert_eq!(r, Some(Decision::Allow));
462
463 let r = a.is_authorized_core(q, &pset, &es);
464 assert!(r
465 .satisfied_permits
466 .contains_key(&PolicyID::from_string("1")));
467 assert!(r.satisfied_forbids.is_empty());
468 assert!(r.residual_permits.contains_key(&PolicyID::from_string("3")));
469 assert!(r.residual_forbids.is_empty());
470 assert!(r.errors.is_empty());
471 }
472
473 #[test]
474 #[cfg(feature = "partial-eval")]
475 fn satisfied_permit_residual_forbid() {
476 let q = Request::new(
477 (EntityUID::with_eid("p"), None),
478 (EntityUID::with_eid("a"), None),
479 (EntityUID::with_eid("r"), None),
480 Context::empty(),
481 None::<&RequestSchemaAllPass>,
482 Extensions::none(),
483 )
484 .unwrap();
485 let a = Authorizer::new();
486 let mut pset = PolicySet::new();
487 let es = Entities::new();
488
489 let src1 = r#"
490 permit(principal,action,resource);
491 "#;
492 let src2 = r#"
493 forbid(principal,action,resource) when {
494 unknown("test")
495 };
496 "#;
497 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("1")), src1).unwrap())
498 .unwrap();
499 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("2")), src2).unwrap())
500 .unwrap();
501
502 let r = a.is_authorized_core(q.clone(), &pset, &es);
503 let map = [("test".into(), Value::from(false))].into_iter().collect();
504 let r2: Response = r.reauthorize(&map, &a, &es).unwrap().into();
505 assert_eq!(r2.decision, Decision::Allow);
506 drop(r2);
507
508 let map = [("test".into(), Value::from(true))].into_iter().collect();
509 let r2: Response = r.reauthorize(&map, &a, &es).unwrap().into();
510 assert_eq!(r2.decision, Decision::Deny);
511
512 let r = a.is_authorized_core(q, &pset, &es);
513 assert!(r
514 .satisfied_permits
515 .contains_key(&PolicyID::from_string("1")));
516 assert!(r.satisfied_forbids.is_empty());
517 assert!(r.errors.is_empty());
518 assert!(r.residual_permits.is_empty());
519 assert!(r.residual_forbids.contains_key(&PolicyID::from_string("2")));
520 }
521
522 #[test]
523 #[cfg(feature = "partial-eval")]
524 fn no_permits() {
525 let q = Request::new(
526 (EntityUID::with_eid("p"), None),
527 (EntityUID::with_eid("a"), None),
528 (EntityUID::with_eid("r"), None),
529 Context::empty(),
530 None::<&RequestSchemaAllPass>,
531 Extensions::none(),
532 )
533 .unwrap();
534 let a = Authorizer::new();
535 let mut pset = PolicySet::new();
536 let es = Entities::new();
537
538 let r = a.is_authorized_core(q.clone(), &pset, &es);
539 assert_eq!(r.decision(), Some(Decision::Deny));
540
541 let src1 = r#"
542 permit(principal, action, resource) when { false };
543 "#;
544
545 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("1")), src1).unwrap())
546 .unwrap();
547 let r = a.is_authorized_core(q.clone(), &pset, &es);
548 assert_eq!(r.decision(), Some(Decision::Deny));
549
550 let src2 = r#"
551 forbid(principal, action, resource) when { unknown("a") };
552 "#;
553
554 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("2")), src2).unwrap())
555 .unwrap();
556 let r = a.is_authorized_core(q.clone(), &pset, &es);
557 assert_eq!(r.decision(), Some(Decision::Deny));
558
559 let src3 = r#"
560 forbid(principal, action, resource) when { true };
561 "#;
562 let src4 = r#"
563 permit(principal, action, resource) when { true };
564 "#;
565
566 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("3")), src3).unwrap())
567 .unwrap();
568 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("4")), src4).unwrap())
569 .unwrap();
570 let r = a.is_authorized_core(q.clone(), &pset, &es);
571 assert_eq!(r.decision(), Some(Decision::Deny));
572
573 let r = a.is_authorized_core(q, &pset, &es);
574 assert!(r
575 .satisfied_permits
576 .contains_key(&PolicyID::from_string("4")));
577 assert!(r
578 .satisfied_forbids
579 .contains_key(&PolicyID::from_string("3")));
580 assert!(r.errors.is_empty());
581 assert!(r.residual_permits.is_empty());
582 assert!(r.residual_forbids.contains_key(&PolicyID::from_string("2")));
583 }
584
585 #[test]
586 #[cfg(feature = "partial-eval")]
587 fn residual_permits() {
588 let q = Request::new(
589 (EntityUID::with_eid("p"), None),
590 (EntityUID::with_eid("a"), None),
591 (EntityUID::with_eid("r"), None),
592 Context::empty(),
593 None::<&RequestSchemaAllPass>,
594 Extensions::none(),
595 )
596 .unwrap();
597 let a = Authorizer::new();
598 let mut pset = PolicySet::new();
599 let es = Entities::new();
600
601 let src1 = r#"
602 permit(principal, action, resource) when { false };
603 "#;
604 let src2 = r#"
605 permit(principal, action, resource) when { unknown("a") };
606 "#;
607 let src3 = r#"
608 forbid(principal, action, resource) when { true };
609 "#;
610
611 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("1")), src1).unwrap())
612 .unwrap();
613 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("2")), src2).unwrap())
614 .unwrap();
615
616 let r = a.is_authorized_core(q.clone(), &pset, &es);
617 let map = [("a".into(), Value::from(false))].into_iter().collect();
618 let r2: Response = r.reauthorize(&map, &a, &es).unwrap().into();
619 assert_eq!(r2.decision, Decision::Deny);
620
621 let map = [("a".into(), Value::from(true))].into_iter().collect();
622 let r2: Response = r.reauthorize(&map, &a, &es).unwrap().into();
623 assert_eq!(r2.decision, Decision::Allow);
624
625 pset.add_static(parser::parse_policy(Some(PolicyID::from_string("3")), src3).unwrap())
626 .unwrap();
627 let r = a.is_authorized_core(q.clone(), &pset, &es);
628 assert_eq!(r.decision(), Some(Decision::Deny));
629
630 let r = a.is_authorized_core(q, &pset, &es);
631 assert!(r.satisfied_permits.is_empty());
632 assert!(r
633 .satisfied_forbids
634 .contains_key(&PolicyID::from_string("3")));
635 assert!(r.errors.is_empty());
636 assert!(r.residual_permits.contains_key(&PolicyID::from_string("2")));
637 assert!(r.residual_forbids.is_empty());
638 }
639}
640#[derive(Debug, PartialEq, Eq, Clone)]
648pub struct Response {
649 pub decision: Decision,
651 pub diagnostics: Diagnostics,
653}
654
655#[derive(Debug, PartialEq, Eq, Clone)]
657pub struct EvaluationResponse {
658 pub satisfied_permits: HashSet<PolicyID>,
660 pub satisfied_forbids: HashSet<PolicyID>,
662 pub errors: Vec<AuthorizationError>,
664 pub permit_residuals: PolicySet,
666 pub forbid_residuals: PolicySet,
668}
669
670#[derive(Debug, PartialEq, Eq, Clone)]
672pub struct Diagnostics {
673 pub reason: HashSet<PolicyID>,
676 pub errors: Vec<AuthorizationError>,
678}
679
680impl Response {
681 pub fn new(
683 decision: Decision,
684 reason: HashSet<PolicyID>,
685 errors: Vec<AuthorizationError>,
686 ) -> Self {
687 Response {
688 decision,
689 diagnostics: Diagnostics { reason, errors },
690 }
691 }
692}
693
694#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
696#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
697#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
698#[serde(rename_all = "camelCase")]
699pub enum Decision {
700 Allow,
702 Deny,
707}