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