Skip to main content

cedar_policy_core/authorizer/
partial_response.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use std::collections::HashMap;
18
19use either::Either;
20use std::sync::Arc;
21
22use super::{
23    Annotations, AuthorizationError, Decision, Effect, EntityUIDEntry, Expr, Policy, Request,
24    Response,
25};
26use crate::{ast::PolicyID, evaluator::EvaluationError};
27
28#[cfg(feature = "partial-eval")]
29use smol_str::SmolStr;
30
31#[cfg(feature = "partial-eval")]
32use crate::{entities::Entities, evaluator::Evaluator};
33
34#[cfg(feature = "partial-eval")]
35use super::{
36    err::{ConcretizationError, ReauthorizationError},
37    Authorizer, Context, PolicySet, PolicySetError, Value,
38};
39
40type PolicyComponents<'a> = (Effect, &'a PolicyID, &'a Arc<Expr>, &'a Arc<Annotations>);
41
42/// Enum representing whether a policy is not satisfied due to
43/// evaluating to `false`, or because it errored.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45pub enum ErrorState {
46    /// The policy did not error
47    NoError,
48    /// The policy did error
49    Error,
50}
51
52/// A partially evaluated authorization response.
53/// Splits the results into several categories: satisfied, false, and residual for each policy effect.
54/// Also tracks all the errors that were encountered during evaluation.
55/// This structure currently has to own all of the `PolicyID` objects due to the [`Self::reauthorize`]
56/// method. If [`PolicySet`] could borrow its PolicyID/contents then this whole structured could be borrowed.
57#[derive(Debug, Clone)]
58pub struct PartialResponse {
59    /// All of the [`Effect::Permit`] policies that were satisfied
60    pub satisfied_permits: HashMap<PolicyID, Arc<Annotations>>,
61    /// All of the [`Effect::Permit`] policies that were not satisfied
62    pub false_permits: HashMap<PolicyID, (ErrorState, Arc<Annotations>)>,
63    /// All of the [`Effect::Permit`] policies that evaluated to a residual
64    pub residual_permits: HashMap<PolicyID, (Arc<Expr>, Arc<Annotations>)>,
65    /// All of the [`Effect::Forbid`] policies that were satisfied
66    pub satisfied_forbids: HashMap<PolicyID, Arc<Annotations>>,
67    /// All of the [`Effect::Forbid`] policies that were not satisfied
68    pub false_forbids: HashMap<PolicyID, (ErrorState, Arc<Annotations>)>,
69    /// All of the [`Effect::Forbid`] policies that evaluated to a residual
70    pub residual_forbids: HashMap<PolicyID, (Arc<Expr>, Arc<Annotations>)>,
71    /// All of the policy errors encountered during evaluation
72    pub errors: Vec<AuthorizationError>,
73    /// The trivial `true` expression, used for materializing a residual for satisfied policies
74    true_expr: Arc<Expr>,
75    /// The trivial `false` expression, used for materializing a residual for non-satisfied policies
76    false_expr: Arc<Expr>,
77    /// The request associated with the partial response
78    #[cfg(feature = "partial-eval")]
79    request: Arc<Request>,
80}
81
82impl PartialResponse {
83    /// Create a partial response from each of the policy result categories
84    #[expect(
85        clippy::too_many_arguments,
86        reason = "maybe in the future we could group these arguments into single-use structs"
87    )]
88    pub fn new(
89        true_permits: impl IntoIterator<Item = (PolicyID, Arc<Annotations>)>,
90        false_permits: impl IntoIterator<Item = (PolicyID, (ErrorState, Arc<Annotations>))>,
91        residual_permits: impl IntoIterator<Item = (PolicyID, (Arc<Expr>, Arc<Annotations>))>,
92        true_forbids: impl IntoIterator<Item = (PolicyID, Arc<Annotations>)>,
93        false_forbids: impl IntoIterator<Item = (PolicyID, (ErrorState, Arc<Annotations>))>,
94        residual_forbids: impl IntoIterator<Item = (PolicyID, (Arc<Expr>, Arc<Annotations>))>,
95        errors: impl IntoIterator<Item = AuthorizationError>,
96        _request: Arc<Request>,
97    ) -> Self {
98        Self {
99            satisfied_permits: true_permits.into_iter().collect(),
100            false_permits: false_permits.into_iter().collect(),
101            residual_permits: residual_permits.into_iter().collect(),
102            satisfied_forbids: true_forbids.into_iter().collect(),
103            false_forbids: false_forbids.into_iter().collect(),
104            residual_forbids: residual_forbids.into_iter().collect(),
105            errors: errors.into_iter().collect(),
106            true_expr: Arc::new(Expr::val(true)),
107            false_expr: Arc::new(Expr::val(false)),
108            #[cfg(feature = "partial-eval")]
109            request: _request,
110        }
111    }
112
113    /// Convert this response into a concrete evaluation response.
114    /// All residuals are treated as errors
115    pub fn concretize(self) -> Response {
116        self.into()
117    }
118
119    /// Attempt to reach a partial decision; the presence of residuals may result in returning [`None`],
120    /// indicating that a decision could not be reached given the unknowns
121    pub fn decision(&self) -> Option<Decision> {
122        match (
123            !self.satisfied_forbids.is_empty(),
124            !self.satisfied_permits.is_empty(),
125            !self.residual_permits.is_empty(),
126            !self.residual_forbids.is_empty(),
127        ) {
128            // Any true forbids means we will deny
129            (true, _, _, _) => Some(Decision::Deny),
130            // No potentially or trivially true permits, means we default deny
131            (_, false, false, _) => Some(Decision::Deny),
132            // Potentially true forbids, means we can't know (as that forbid may evaluate to true, overriding any permits)
133            (false, _, _, true) => None,
134            // No true permits, but some potentially true permits + no true/potentially true forbids means we don't know
135            (false, false, true, false) => None,
136            // At least one trivially true permit, and no trivially or possible true forbids, means we allow
137            (false, true, _, false) => Some(Decision::Allow),
138        }
139    }
140
141    /// All of the [`Effect::Permit`] policies that were known to be satisfied
142    fn definitely_satisfied_permits(&self) -> impl Iterator<Item = Policy> + '_ {
143        self.satisfied_permits.iter().map(|(id, annotations)| {
144            construct_policy((Effect::Permit, id, &self.true_expr, annotations))
145        })
146    }
147
148    /// All of the [`Effect::Forbid`] policies that were known to be satisfied
149    fn definitely_satisfied_forbids(&self) -> impl Iterator<Item = Policy> + '_ {
150        self.satisfied_forbids.iter().map(|(id, annotations)| {
151            construct_policy((Effect::Forbid, id, &self.true_expr, annotations))
152        })
153    }
154
155    /// Returns the set of [`PolicyID`]s that were definitely satisfied -- both permits and forbids
156    pub fn definitely_satisfied(&self) -> impl Iterator<Item = Policy> + '_ {
157        self.definitely_satisfied_permits()
158            .chain(self.definitely_satisfied_forbids())
159    }
160
161    /// Returns the set of [`PolicyID`]s that encountered errors
162    pub fn definitely_errored(&self) -> impl Iterator<Item = &PolicyID> {
163        self.false_permits
164            .iter()
165            .chain(self.false_forbids.iter())
166            .filter_map(did_error)
167    }
168
169    /// Returns an over-approximation of the set of determining policies.
170    ///
171    /// This is all policies that may be determining for any substitution of the unknowns.
172    pub fn may_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
173        if self.satisfied_forbids.is_empty() {
174            // We have no definitely true forbids, so the over approx is everything that is true or potentially true
175            Either::Left(
176                self.definitely_satisfied_permits()
177                    .chain(self.residual_permits())
178                    .chain(self.residual_forbids()),
179            )
180        } else {
181            // We have definitely true forbids, so we know only things that can determine is
182            // true forbids and potentially true forbids
183            Either::Right(
184                self.definitely_satisfied_forbids()
185                    .chain(self.residual_forbids()),
186            )
187        }
188    }
189
190    fn residual_permits(&self) -> impl Iterator<Item = Policy> + '_ {
191        self.residual_permits
192            .iter()
193            .map(|(id, (expr, annotations))| {
194                construct_policy((Effect::Permit, id, expr, annotations))
195            })
196    }
197
198    fn residual_forbids(&self) -> impl Iterator<Item = Policy> + '_ {
199        self.residual_forbids
200            .iter()
201            .map(|(id, (expr, annotations))| {
202                construct_policy((Effect::Forbid, id, expr, annotations))
203            })
204    }
205
206    /// Returns an under-approximation of the set of determining policies.
207    ///
208    /// This is all policies that must be determining for all possible substitutions of the unknowns.
209    pub fn must_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
210        // If there are no true forbids or potentially true forbids,
211        // then the under approximation is the true permits
212        if self.satisfied_forbids.is_empty() && self.residual_forbids.is_empty() {
213            Either::Left(self.definitely_satisfied_permits())
214        } else {
215            // Otherwise it's the true forbids
216            Either::Right(self.definitely_satisfied_forbids())
217        }
218    }
219
220    /// Returns the set of non-trivial (meaning more than just `true` or `false`) residuals expressions
221    pub fn nontrivial_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
222        self.nontrivial_permits().chain(self.nontrivial_forbids())
223    }
224
225    /// Returns the set of ids of non-trivial (meaning more than just `true` or `false`) residuals expressions
226    pub fn nontrivial_residual_ids(&self) -> impl Iterator<Item = &PolicyID> {
227        self.residual_permits
228            .keys()
229            .chain(self.residual_forbids.keys())
230    }
231
232    /// Returns the set of non-trivial (meaning more than just `true` or `false`) residuals expressions from [`Effect::Permit`]
233    fn nontrivial_permits(&self) -> impl Iterator<Item = Policy> + '_ {
234        self.residual_permits
235            .iter()
236            .map(|(id, (expr, annotations))| {
237                construct_policy((Effect::Permit, id, expr, annotations))
238            })
239    }
240
241    /// Returns the set of non-trivial (meaning more than just `true` or `false`) residuals expressions from [`Effect::Forbid`]
242    pub fn nontrivial_forbids(&self) -> impl Iterator<Item = Policy> + '_ {
243        self.residual_forbids
244            .iter()
245            .map(|(id, (expr, annotations))| {
246                construct_policy((Effect::Forbid, id, expr, annotations))
247            })
248    }
249
250    /// Returns every policy residual, including trivial ones
251    pub fn all_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
252        self.all_permit_residuals()
253            .chain(self.all_forbid_residuals())
254            .map(construct_policy)
255    }
256
257    /// Returns all residuals expressions that come from [`Effect::Permit`] policies
258    fn all_permit_residuals(&'_ self) -> impl Iterator<Item = PolicyComponents<'_>> {
259        let trues = self
260            .satisfied_permits
261            .iter()
262            .map(|(id, a)| (id, (&self.true_expr, a)));
263        let falses = self
264            .false_permits
265            .iter()
266            .map(|(id, (_, a))| (id, (&self.false_expr, a)));
267        let nontrivial = self
268            .residual_permits
269            .iter()
270            .map(|(id, (r, a))| (id, (r, a)));
271        trues
272            .chain(falses)
273            .chain(nontrivial)
274            .map(|(id, (r, a))| (Effect::Permit, id, r, a))
275    }
276
277    /// Returns all residuals expressions that come from [`Effect::Forbid`] policies
278    fn all_forbid_residuals(&'_ self) -> impl Iterator<Item = PolicyComponents<'_>> {
279        let trues = self
280            .satisfied_forbids
281            .iter()
282            .map(|(id, a)| (id, (&self.true_expr, a)));
283        let falses = self
284            .false_forbids
285            .iter()
286            .map(|(id, (_, a))| (id, (&self.false_expr, a)));
287        let nontrivial = self
288            .residual_forbids
289            .iter()
290            .map(|(id, (r, a))| (id, (r, a)));
291        trues
292            .chain(falses)
293            .chain(nontrivial)
294            .map(|(id, (r, a))| (Effect::Forbid, id, r, a))
295    }
296
297    /// Return the residual for a given [`PolicyID`], if it exists in the response
298    pub fn get(&self, id: &PolicyID) -> Option<Policy> {
299        self.get_permit(id).or_else(|| self.get_forbid(id))
300    }
301
302    fn get_permit(&self, id: &PolicyID) -> Option<Policy> {
303        self.residual_permits
304            .get(id)
305            .map(|(a, b)| (a, b))
306            .or_else(|| self.satisfied_permits.get(id).map(|a| (&self.true_expr, a)))
307            .or_else(|| {
308                self.false_permits
309                    .get(id)
310                    .map(|(_, a)| (&self.false_expr, a))
311            })
312            .map(|(expr, a)| construct_policy((Effect::Permit, id, expr, a)))
313    }
314
315    fn get_forbid(&self, id: &PolicyID) -> Option<Policy> {
316        self.residual_forbids
317            .get(id)
318            .map(|(a, b)| (a, b))
319            .or_else(|| self.satisfied_forbids.get(id).map(|a| (&self.true_expr, a)))
320            .or_else(|| {
321                self.false_forbids
322                    .get(id)
323                    .map(|(_, a)| (&self.false_expr, a))
324            })
325            .map(|(expr, a)| construct_policy((Effect::Forbid, id, expr, a)))
326    }
327
328    /// Attempt to re-authorize this response given a mapping from unknowns to values
329    #[cfg(feature = "partial-eval")]
330    pub fn reauthorize(
331        &self,
332        mapping: &HashMap<SmolStr, Value>,
333        auth: &Authorizer,
334        es: &Entities,
335    ) -> Result<Self, ReauthorizationError> {
336        let policyset = self.all_residual_policies()?;
337        let new_request = self.concretize_request(mapping)?;
338        // Although this function takes a HashMap, keep the internal mapping function generic
339        let unknowns_mapper =
340            |unknown_name: &str| -> Option<Value> { mapping.get(unknown_name).cloned() };
341        // Construct an evaluator resolving these specific unknown mappings
342        let eval = Evaluator::new(new_request.clone(), es, auth.extensions)
343            .with_unknowns_mapper(Box::new(unknowns_mapper));
344        Ok(auth.is_authorized_core_internal(&eval, new_request, &policyset))
345    }
346
347    #[cfg(feature = "partial-eval")]
348    fn all_residual_policies(&self) -> Result<PolicySet, PolicySetError> {
349        PolicySet::try_from_iter(
350            self.all_permit_residuals()
351                .chain(self.all_forbid_residuals())
352                .map(|(effect, id, expr, annotations)| {
353                    Policy::from_when_clause_annos(
354                        effect,
355                        expr.clone(),
356                        id.clone(),
357                        expr.source_loc().cloned(),
358                        annotations.clone(),
359                    )
360                }),
361        )
362    }
363
364    #[cfg(feature = "partial-eval")]
365    fn concretize_request(
366        &self,
367        mapping: &HashMap<SmolStr, Value>,
368    ) -> Result<Request, ConcretizationError> {
369        let mut context = self.request.context.clone();
370
371        let principal = self.request.principal().concretize("principal", mapping)?;
372
373        let action = self.request.action.concretize("action", mapping)?;
374
375        let resource = self.request.resource.concretize("resource", mapping)?;
376
377        if let Some((key, val)) = mapping.get_key_value("context") {
378            if let Ok(attrs) = val.get_as_record() {
379                match self.request.context() {
380                    Some(ctx) => {
381                        return Err(ConcretizationError::VarConfictError {
382                            id: key.to_owned(),
383                            existing_value: ctx.clone().into(),
384                            given_value: val.clone(),
385                        });
386                    }
387                    None => context = Some(Context::Value(attrs.clone())),
388                }
389            } else {
390                return Err(ConcretizationError::ValueError {
391                    id: key.to_owned(),
392                    expected_type: "record",
393                    given_value: val.to_owned(),
394                });
395            }
396        }
397
398        // We need to replace unknowns in the partial context as well
399        context = context
400            .map(|context| context.substitute(mapping))
401            .transpose()?;
402
403        Ok(Request {
404            principal,
405            action,
406            resource,
407            context,
408        })
409    }
410
411    fn errors(self) -> impl Iterator<Item = AuthorizationError> {
412        self.residual_forbids
413            .into_iter()
414            .chain(self.residual_permits)
415            .map(
416                |(id, (expr, _))| AuthorizationError::PolicyEvaluationError {
417                    id,
418                    error: EvaluationError::non_value(expr.as_ref().clone()),
419                },
420            )
421            .chain(self.errors)
422            .collect::<Vec<_>>()
423            .into_iter()
424    }
425}
426
427impl EntityUIDEntry {
428    #[cfg(feature = "partial-eval")]
429    fn concretize(
430        &self,
431        key: &str,
432        mapping: &HashMap<SmolStr, Value>,
433    ) -> Result<Self, ConcretizationError> {
434        if let Some(val) = mapping.get(key) {
435            if let Ok(uid) = val.get_as_entity() {
436                match self {
437                    EntityUIDEntry::Known { euid, .. } => {
438                        Err(ConcretizationError::VarConfictError {
439                            id: key.into(),
440                            existing_value: euid.as_ref().clone().into(),
441                            given_value: val.clone(),
442                        })
443                    }
444                    EntityUIDEntry::Unknown { ty: None, .. } => {
445                        Ok(EntityUIDEntry::known(uid.clone(), None))
446                    }
447                    EntityUIDEntry::Unknown {
448                        ty: Some(type_of_unknown),
449                        ..
450                    } => {
451                        if type_of_unknown == uid.entity_type() {
452                            Ok(EntityUIDEntry::known(uid.clone(), None))
453                        } else {
454                            Err(ConcretizationError::EntityTypeConfictError {
455                                id: key.into(),
456                                existing_value: type_of_unknown.clone(),
457                                given_value: val.to_owned(),
458                            })
459                        }
460                    }
461                }
462            } else {
463                Err(ConcretizationError::ValueError {
464                    id: key.into(),
465                    expected_type: "entity",
466                    given_value: val.to_owned(),
467                })
468            }
469        } else {
470            Ok(self.clone())
471        }
472    }
473}
474
475impl From<PartialResponse> for Response {
476    fn from(p: PartialResponse) -> Self {
477        let decision = if !p.satisfied_permits.is_empty() && p.satisfied_forbids.is_empty() {
478            Decision::Allow
479        } else {
480            Decision::Deny
481        };
482        Response::new(
483            decision,
484            p.must_be_determining().map(|p| p.id().clone()).collect(),
485            p.errors().collect(),
486        )
487    }
488}
489
490/// Build a policy from a policy components
491fn construct_policy((effect, id, expr, annotations): PolicyComponents<'_>) -> Policy {
492    Policy::from_when_clause_annos(
493        effect,
494        expr.clone(),
495        id.clone(),
496        expr.source_loc().cloned(),
497        (*annotations).clone(),
498    )
499}
500
501/// Checks if a given residual record did error, returning the [`PolicyID`] if it did
502fn did_error<'a>(
503    (id, (state, _)): (&'a PolicyID, &'_ (ErrorState, Arc<Annotations>)),
504) -> Option<&'a PolicyID> {
505    match *state {
506        ErrorState::NoError => None,
507        ErrorState::Error => Some(id),
508    }
509}
510
511#[cfg(test)]
512mod test {
513    use std::{
514        collections::HashSet,
515        iter::{empty, once},
516    };
517
518    // An extremely slow and bad set, but it only requires that the contents be [`PartialEq`]
519    // Using this because I don't want to enforce an output order on the tests, but policies can't easily be Hash or Ord
520    #[derive(Debug, Default)]
521    struct SlowSet<T> {
522        contents: Vec<T>,
523    }
524
525    impl<T: PartialEq> SlowSet<T> {
526        pub fn from(iter: impl IntoIterator<Item = T>) -> Self {
527            let mut contents = vec![];
528            for item in iter.into_iter() {
529                if !contents.contains(&item) {
530                    contents.push(item)
531                }
532            }
533            Self { contents }
534        }
535
536        pub fn len(&self) -> usize {
537            self.contents.len()
538        }
539
540        pub fn contains(&self, item: &T) -> bool {
541            self.contents.contains(item)
542        }
543    }
544
545    impl<T: PartialEq> PartialEq for SlowSet<T> {
546        fn eq(&self, rhs: &Self) -> bool {
547            if self.len() == rhs.len() {
548                self.contents.iter().all(|item| rhs.contains(item))
549            } else {
550                false
551            }
552        }
553    }
554
555    impl<T: PartialEq> FromIterator<T> for SlowSet<T> {
556        fn from_iter<I>(iter: I) -> Self
557        where
558            I: IntoIterator<Item = T>,
559        {
560            Self::from(iter)
561        }
562    }
563
564    use crate::authorizer::{ActionConstraint, Context, PrincipalConstraint, ResourceConstraint};
565
566    #[cfg(feature = "partial-eval")]
567    use crate::{
568        authorizer::{EntityUID, RestrictedExpr, Unknown},
569        extensions::Extensions,
570        parser::parse_policyset,
571        FromNormalizedStr,
572    };
573
574    use super::*;
575
576    #[test]
577    fn sanity_check() {
578        let empty_annotations: Arc<Annotations> = Arc::default();
579        let one_plus_two = Arc::new(Expr::add(Expr::val(1), Expr::val(2)));
580        let three_plus_four = Arc::new(Expr::add(Expr::val(3), Expr::val(4)));
581        let a = once((PolicyID::from_string("a"), empty_annotations.clone()));
582        let bc = [
583            (
584                PolicyID::from_string("b"),
585                (ErrorState::Error, empty_annotations.clone()),
586            ),
587            (
588                PolicyID::from_string("c"),
589                (ErrorState::NoError, empty_annotations.clone()),
590            ),
591        ];
592        let d = once((
593            PolicyID::from_string("d"),
594            (one_plus_two.clone(), empty_annotations.clone()),
595        ));
596        let e = once((PolicyID::from_string("e"), empty_annotations.clone()));
597        let fg = [
598            (
599                PolicyID::from_string("f"),
600                (ErrorState::Error, empty_annotations.clone()),
601            ),
602            (
603                PolicyID::from_string("g"),
604                (ErrorState::NoError, empty_annotations.clone()),
605            ),
606        ];
607        let h = once((
608            PolicyID::from_string("h"),
609            (three_plus_four.clone(), empty_annotations),
610        ));
611        let errs = empty();
612        let pr = PartialResponse::new(
613            a,
614            bc,
615            d,
616            e,
617            fg,
618            h,
619            errs,
620            Arc::new(Request::new_unchecked(
621                EntityUIDEntry::unknown(),
622                EntityUIDEntry::unknown(),
623                EntityUIDEntry::unknown(),
624                Some(Context::empty()),
625            )),
626        );
627
628        let a = Policy::from_when_clause(
629            Effect::Permit,
630            Expr::val(true),
631            PolicyID::from_string("a"),
632            None,
633        );
634        let b = Policy::from_when_clause(
635            Effect::Permit,
636            Expr::val(false),
637            PolicyID::from_string("b"),
638            None,
639        );
640        let c = Policy::from_when_clause(
641            Effect::Permit,
642            Expr::val(false),
643            PolicyID::from_string("c"),
644            None,
645        );
646        let d = Policy::from_when_clause_annos(
647            Effect::Permit,
648            one_plus_two,
649            PolicyID::from_string("d"),
650            None,
651            Arc::default(),
652        );
653        let e = Policy::from_when_clause(
654            Effect::Forbid,
655            Expr::val(true),
656            PolicyID::from_string("e"),
657            None,
658        );
659        let f = Policy::from_when_clause(
660            Effect::Forbid,
661            Expr::val(false),
662            PolicyID::from_string("f"),
663            None,
664        );
665        let g = Policy::from_when_clause(
666            Effect::Forbid,
667            Expr::val(false),
668            PolicyID::from_string("g"),
669            None,
670        );
671        let h = Policy::from_when_clause_annos(
672            Effect::Forbid,
673            three_plus_four,
674            PolicyID::from_string("h"),
675            None,
676            Arc::default(),
677        );
678
679        assert_eq!(
680            pr.definitely_satisfied_permits().collect::<SlowSet<_>>(),
681            SlowSet::from([a.clone()])
682        );
683        assert_eq!(
684            pr.definitely_satisfied_forbids().collect::<SlowSet<_>>(),
685            SlowSet::from([e.clone()])
686        );
687        assert_eq!(
688            pr.definitely_satisfied().collect::<SlowSet<_>>(),
689            SlowSet::from([a.clone(), e.clone()])
690        );
691        assert_eq!(
692            pr.definitely_errored().collect::<HashSet<_>>(),
693            HashSet::from([&PolicyID::from_string("b"), &PolicyID::from_string("f")])
694        );
695        assert_eq!(
696            pr.may_be_determining().collect::<SlowSet<_>>(),
697            SlowSet::from([e.clone(), h.clone()])
698        );
699        assert_eq!(
700            pr.must_be_determining().collect::<SlowSet<_>>(),
701            SlowSet::from([e.clone()])
702        );
703        assert_eq!(pr.nontrivial_residuals().count(), 2);
704
705        assert_eq!(
706            pr.nontrivial_residuals().collect::<SlowSet<_>>(),
707            SlowSet::from([d.clone(), h.clone()])
708        );
709        assert_eq!(
710            pr.all_residuals().collect::<SlowSet<_>>(),
711            SlowSet::from([&a, &b, &c, &d, &e, &f, &g, &h].into_iter().cloned())
712        );
713        assert_eq!(
714            pr.nontrivial_residual_ids().collect::<HashSet<_>>(),
715            HashSet::from([&PolicyID::from_string("d"), &PolicyID::from_string("h")])
716        );
717
718        assert_eq!(pr.get(&PolicyID::from_string("a")), Some(a));
719        assert_eq!(pr.get(&PolicyID::from_string("b")), Some(b));
720        assert_eq!(pr.get(&PolicyID::from_string("c")), Some(c));
721        assert_eq!(pr.get(&PolicyID::from_string("d")), Some(d));
722        assert_eq!(pr.get(&PolicyID::from_string("e")), Some(e));
723        assert_eq!(pr.get(&PolicyID::from_string("f")), Some(f));
724        assert_eq!(pr.get(&PolicyID::from_string("g")), Some(g));
725        assert_eq!(pr.get(&PolicyID::from_string("h")), Some(h));
726        assert_eq!(pr.get(&PolicyID::from_string("i")), None);
727    }
728
729    #[test]
730    fn build_policies_trivial_permit() {
731        let e = Arc::new(Expr::add(Expr::val(1), Expr::val(2)));
732        let id = PolicyID::from_string("foo");
733        let p = construct_policy((Effect::Permit, &id, &e, &Arc::default()));
734        assert_eq!(p.effect(), Effect::Permit);
735        assert!(p.annotations().next().is_none());
736        assert_eq!(p.action_constraint(), &ActionConstraint::Any);
737        assert_eq!(p.principal_constraint(), PrincipalConstraint::any());
738        assert_eq!(p.resource_constraint(), ResourceConstraint::any());
739        assert_eq!(p.id(), &id);
740        assert_eq!(p.non_scope_constraints(), Some(e.as_ref()));
741    }
742
743    #[test]
744    fn build_policies_trivial_forbid() {
745        let e = Arc::new(Expr::add(Expr::val(1), Expr::val(2)));
746        let id = PolicyID::from_string("foo");
747        let p = construct_policy((Effect::Forbid, &id, &e, &Arc::default()));
748        assert_eq!(p.effect(), Effect::Forbid);
749        assert!(p.annotations().next().is_none());
750        assert_eq!(p.action_constraint(), &ActionConstraint::Any);
751        assert_eq!(p.principal_constraint(), PrincipalConstraint::any());
752        assert_eq!(p.resource_constraint(), ResourceConstraint::any());
753        assert_eq!(p.id(), &id);
754        assert_eq!(p.non_scope_constraints(), Some(e.as_ref()));
755    }
756
757    #[test]
758    fn did_error_error() {
759        assert_eq!(
760            did_error((
761                &PolicyID::from_string("foo"),
762                &(ErrorState::Error, Arc::default())
763            )),
764            Some(&PolicyID::from_string("foo"))
765        );
766    }
767
768    #[test]
769    fn did_error_noerror() {
770        assert_eq!(
771            did_error((
772                &PolicyID::from_string("foo"),
773                &(ErrorState::NoError, Arc::default())
774            )),
775            None,
776        );
777    }
778
779    #[test]
780    #[cfg(feature = "partial-eval")]
781    fn reauthorize() {
782        let policies = parse_policyset(
783            r#"
784            permit(principal, action, resource) when {
785                principal == NS::"a" && resource == NS::"b"
786            };
787            forbid(principal, action, resource) when {
788                context.c && context.d == 10
789            };
790        "#,
791        )
792        .unwrap();
793
794        let context_unknown = Context::from_pairs(
795            [
796                (
797                    "c".into(),
798                    RestrictedExpr::unknown(Unknown::new_untyped("c")),
799                ),
800                (
801                    "d".into(),
802                    RestrictedExpr::unknown(Unknown::new_untyped("d")),
803                ),
804            ],
805            Extensions::all_available(),
806        )
807        .unwrap();
808
809        let partial_request = Request {
810            principal: EntityUIDEntry::known(r#"NS::"a""#.parse().unwrap(), None),
811            action: EntityUIDEntry::unknown(),
812            resource: EntityUIDEntry::unknown(),
813            context: Some(context_unknown),
814        };
815
816        let entities = Entities::new();
817
818        let authorizer = Authorizer::new();
819        let partial_response = authorizer.is_authorized_core(partial_request, &policies, &entities);
820
821        let response_with_concrete_resource = partial_response
822            .reauthorize(
823                &HashMap::from([(
824                    "resource".into(),
825                    EntityUID::from_normalized_str(r#"NS::"b""#).unwrap().into(),
826                )]),
827                &authorizer,
828                &entities,
829            )
830            .unwrap();
831
832        assert_eq!(
833            response_with_concrete_resource
834                .get(&PolicyID::from_string("policy0"))
835                .map(|p| p.effect()),
836            Some(Effect::Permit)
837        );
838
839        let response_with_concrete_context_attr = response_with_concrete_resource
840            .reauthorize(
841                &HashMap::from([("c".into(), true.into()), ("d".into(), 10.into())]),
842                &authorizer,
843                &entities,
844            )
845            .unwrap();
846
847        // Expected result, as the presence of context.c == true && context.d == 10 makes the deny policy apply
848        assert_eq!(
849            response_with_concrete_context_attr.decision(),
850            Some(Decision::Deny)
851        );
852    }
853}