Skip to main content

cedar_policy_core/tpe/
request.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
17//! This module contains the partial request.
18
19use std::{collections::BTreeMap, sync::Arc};
20
21use crate::ast::{EntityUIDEntry, RequestSchema};
22use crate::entities::conformance::err::InvalidEnumEntityError;
23use crate::tpe::err::{
24    ExistingPrincipalError, ExistingResourceError, InconsistentActionError,
25    InconsistentPrincipalEidError, InconsistentPrincipalTypeError, InconsistentResourceEidError,
26    InconsistentResourceTypeError, IncorrectPrincipalEntityTypeError,
27    IncorrectResourceEntityTypeError, NoMatchingReqEnvError, RequestBuilderError,
28    RequestConsistencyError,
29};
30use crate::validator::request_validation_errors::{
31    UndeclaredActionError, UndeclaredPrincipalTypeError, UndeclaredResourceTypeError,
32};
33use crate::validator::{
34    types::RequestEnv, RequestValidationError, ValidationMode, ValidatorEntityTypeKind,
35    ValidatorSchema,
36};
37use crate::{
38    ast::{Context, Eid, EntityType, EntityUID, Request, Value},
39    entities::conformance::is_valid_enumerated_entity,
40    extensions::Extensions,
41};
42use smol_str::SmolStr;
43
44/// Partial EntityUID
45#[derive(Debug, Clone)]
46pub struct PartialEntityUID {
47    /// Typename of the entity
48    pub ty: EntityType,
49    /// Optional EID of the entity
50    pub eid: Option<Eid>,
51}
52
53#[derive(Debug)]
54enum PartialEUIDConsistencyError {
55    Unknown,
56    InconsistentType(EntityType, EntityType),
57    InconsistentEid(Eid, Eid),
58}
59
60impl PartialEUIDConsistencyError {
61    pub fn into_resource_error(self) -> RequestConsistencyError {
62        match self {
63            PartialEUIDConsistencyError::Unknown => RequestConsistencyError::UnknownResource,
64            PartialEUIDConsistencyError::InconsistentType(partial, concrete) => {
65                InconsistentResourceTypeError { partial, concrete }.into()
66            }
67            PartialEUIDConsistencyError::InconsistentEid(partial, concrete) => {
68                InconsistentResourceEidError { partial, concrete }.into()
69            }
70        }
71    }
72
73    pub fn into_principal_error(self) -> RequestConsistencyError {
74        match self {
75            PartialEUIDConsistencyError::Unknown => RequestConsistencyError::UnknownPrincipal,
76            PartialEUIDConsistencyError::InconsistentType(partial, concrete) => {
77                InconsistentPrincipalTypeError { partial, concrete }.into()
78            }
79            PartialEUIDConsistencyError::InconsistentEid(partial, concrete) => {
80                InconsistentPrincipalEidError { partial, concrete }.into()
81            }
82        }
83    }
84}
85
86#[derive(Debug)]
87enum PartialEUIDValidationError {
88    UndeclaredType(EntityType),
89    InvalidEnum(InvalidEnumEntityError),
90}
91
92impl PartialEUIDValidationError {
93    pub fn into_resource_error(self) -> RequestValidationError {
94        match self {
95            PartialEUIDValidationError::UndeclaredType(resource_ty) => {
96                UndeclaredResourceTypeError { resource_ty }.into()
97            }
98            PartialEUIDValidationError::InvalidEnum(enum_err) => enum_err.into(),
99        }
100    }
101
102    pub fn into_principal_error(self) -> RequestValidationError {
103        match self {
104            PartialEUIDValidationError::UndeclaredType(principal_ty) => {
105                UndeclaredPrincipalTypeError { principal_ty }.into()
106            }
107            PartialEUIDValidationError::InvalidEnum(enum_err) => enum_err.into(),
108        }
109    }
110}
111
112#[derive(Debug)]
113enum PartialEUIDBuilderError {
114    Existing(EntityUID),
115    IncorrectType(EntityType, EntityType),
116    Invalid(PartialEUIDValidationError),
117}
118
119impl PartialEUIDBuilderError {
120    pub fn into_resource_error(self) -> RequestBuilderError {
121        match self {
122            PartialEUIDBuilderError::Existing(resource) => {
123                ExistingResourceError { resource }.into()
124            }
125            PartialEUIDBuilderError::IncorrectType(ty, expected) => {
126                IncorrectResourceEntityTypeError { ty, expected }.into()
127            }
128            PartialEUIDBuilderError::Invalid(e) => e.into_resource_error().into(),
129        }
130    }
131
132    pub fn into_principal_error(self) -> RequestBuilderError {
133        match self {
134            PartialEUIDBuilderError::Existing(principal) => {
135                ExistingPrincipalError { principal }.into()
136            }
137            PartialEUIDBuilderError::IncorrectType(ty, expected) => {
138                IncorrectPrincipalEntityTypeError { ty, expected }.into()
139            }
140            PartialEUIDBuilderError::Invalid(e) => e.into_principal_error().into(),
141        }
142    }
143}
144
145impl PartialEntityUID {
146    fn check_type(
147        &self,
148        schema: &ValidatorSchema,
149        uid: Option<&EntityUID>,
150    ) -> Result<(), PartialEUIDValidationError> {
151        // Entity type must be declared
152        let entity_ty = schema
153            .get_entity_type(&self.ty)
154            .ok_or_else(|| PartialEUIDValidationError::UndeclaredType(self.ty.clone()))?;
155        // If we have a concrete uid and this is an enum entity, it must be one
156        // of the choices
157        if let (ValidatorEntityTypeKind::Enum(choices), Some(uid)) = (&entity_ty.kind, uid) {
158            is_valid_enumerated_entity(choices, uid)
159                .map_err(PartialEUIDValidationError::InvalidEnum)?;
160        }
161        Ok(())
162    }
163
164    fn validate(&self, schema: &ValidatorSchema) -> Result<(), PartialEUIDValidationError> {
165        self.check_type(schema, EntityUID::try_from(self.clone()).ok().as_ref())
166    }
167
168    fn check_consistency(&self, entry: &EntityUIDEntry) -> Result<(), PartialEUIDConsistencyError> {
169        let EntityUIDEntry::Known { euid, .. } = entry else {
170            return Err(PartialEUIDConsistencyError::Unknown);
171        };
172        if euid.entity_type() != &self.ty {
173            return Err(PartialEUIDConsistencyError::InconsistentType(
174                self.ty.clone(),
175                euid.entity_type().clone(),
176            ));
177        }
178        if let Some(eid) = &self.eid {
179            if eid != euid.eid() {
180                return Err(PartialEUIDConsistencyError::InconsistentEid(
181                    eid.clone(),
182                    euid.eid().clone(),
183                ));
184            }
185        }
186        Ok(())
187    }
188
189    /// Attempt to fill an unknown euid in a request with a concrete candidate.
190    ///
191    /// Errors without changing `self` if the candidate is in incompatible
192    fn set_candidate(
193        &mut self,
194        candidate: EntityUID,
195        schema: &ValidatorSchema,
196    ) -> Result<(), PartialEUIDBuilderError> {
197        if let Some(eid) = &self.eid {
198            return Err(PartialEUIDBuilderError::Existing(
199                EntityUID::from_components(self.ty.clone(), eid.clone(), None),
200            ));
201        }
202        if candidate.entity_type() != &self.ty {
203            return Err(PartialEUIDBuilderError::IncorrectType(
204                candidate.entity_type().clone(),
205                self.ty.clone(),
206            ));
207        }
208        self.check_type(schema, Some(&candidate))
209            .map_err(PartialEUIDBuilderError::Invalid)?;
210        *self = PartialEntityUID::from(candidate);
211        Ok(())
212    }
213}
214
215impl TryFrom<PartialEntityUID> for EntityUID {
216    type Error = ();
217    fn try_from(value: PartialEntityUID) -> Result<EntityUID, ()> {
218        if let Some(eid) = value.eid {
219            Ok(EntityUID::from_components(value.ty, eid, None))
220        } else {
221            Err(())
222        }
223    }
224}
225
226impl From<EntityUID> for PartialEntityUID {
227    fn from(value: EntityUID) -> Self {
228        let (ty, eid) = value.components();
229        Self { ty, eid: Some(eid) }
230    }
231}
232
233/// Represents the request tuple <P, A, R, C> (see the Cedar design doc).
234#[derive(Debug, Clone)]
235pub struct PartialRequest {
236    /// Principal associated with the request
237    principal: PartialEntityUID,
238
239    /// Action associated with the request
240    action: EntityUID,
241
242    /// Resource associated with the request
243    resource: PartialEntityUID,
244
245    /// Context associated with the request.
246    /// `None` means that variable will result in a residual for partial evaluation.
247    context: Option<Arc<BTreeMap<SmolStr, Value>>>,
248}
249
250impl PartialRequest {
251    /// Create a well-formed `PartialRequest` (i.e., it conforms to the schema)
252    pub fn new(
253        principal: PartialEntityUID,
254        action: EntityUID,
255        resource: PartialEntityUID,
256        context: Option<Arc<BTreeMap<SmolStr, Value>>>,
257        schema: &ValidatorSchema,
258    ) -> Result<Self, RequestValidationError> {
259        let req = Self {
260            principal,
261            action,
262            resource,
263            context,
264        };
265        req.validate(schema)?;
266        Ok(req)
267    }
268
269    // Find the matching `RequestEnv`
270    pub(crate) fn find_request_env<'s>(
271        &self,
272        schema: &'s ValidatorSchema,
273    ) -> Result<RequestEnv<'s>, NoMatchingReqEnvError> {
274        #[expect(
275            clippy::unwrap_used,
276            reason = "strict validation should produce concrete action entity uid"
277        )]
278        schema
279            .unlinked_request_envs(ValidationMode::Strict)
280            .find(|env| {
281                env.action_entity_uid().unwrap() == &self.action
282                    && env.principal_entity_type() == Some(&self.principal.ty)
283                    && env.resource_entity_type() == Some(&self.resource.ty)
284            })
285            .ok_or(NoMatchingReqEnvError)
286    }
287
288    // Validate `self` with `schema`
289    pub(crate) fn validate(&self, schema: &ValidatorSchema) -> Result<(), RequestValidationError> {
290        if let Some(action_id) = schema.get_action_id(&self.action) {
291            action_id.check_principal_type(&self.principal.ty, &self.action.clone().into())?;
292            action_id.check_resource_type(&self.resource.ty, &self.action.clone().into())?;
293            self.principal
294                .validate(schema)
295                .map_err(|e| e.into_principal_error())?;
296            self.resource
297                .validate(schema)
298                .map_err(|e| e.into_resource_error())?;
299            if let Some(m) = &self.context {
300                schema.validate_context(
301                    &Context::Value(m.clone()),
302                    &self.action,
303                    Extensions::all_available(),
304                )?;
305            }
306            Ok(())
307        } else {
308            Err(UndeclaredActionError {
309                action: self.action.clone().into(),
310            }
311            .into())
312        }
313    }
314
315    /// Check consistency between a [`PartialRequest`] and a [`Request`]
316    pub fn check_consistency(&self, request: &Request) -> Result<(), RequestConsistencyError> {
317        self.principal
318            .check_consistency(&request.principal)
319            .map_err(|e| e.into_principal_error())?;
320        self.resource
321            .check_consistency(&request.resource)
322            .map_err(|e| e.into_resource_error())?;
323
324        match &request.action {
325            EntityUIDEntry::Unknown { .. } => {
326                return Err(RequestConsistencyError::UnknownAction);
327            }
328            EntityUIDEntry::Known { euid, .. } => {
329                if euid.as_ref() != &self.action {
330                    return Err(InconsistentActionError {
331                        partial: self.action.clone(),
332                        concrete: euid.as_ref().clone(),
333                    }
334                    .into());
335                }
336            }
337        }
338
339        match &request.context {
340            Some(Context::Value(c)) => {
341                if let Some(m) = &self.context {
342                    if c != m {
343                        return Err(RequestConsistencyError::InconsistentContext);
344                    }
345                }
346            }
347            Some(Context::RestrictedResidual { .. }) => {
348                return Err(RequestConsistencyError::ConcreteContextContainsUnknowns);
349            }
350            None => {
351                return Err(RequestConsistencyError::UnknownContext);
352            }
353        }
354        Ok(())
355    }
356
357    /// Get the [`EntityType`] of `principal`
358    pub fn principal_type(&self) -> &EntityType {
359        &self.principal.ty
360    }
361
362    /// Get the [`EntityType`] of `resource`
363    pub fn resource_type(&self) -> &EntityType {
364        &self.resource.ty
365    }
366
367    /// Get the `principal`
368    pub fn principal(&self) -> &PartialEntityUID {
369        &self.principal
370    }
371
372    /// Get the `resource`
373    pub fn resource(&self) -> &PartialEntityUID {
374        &self.resource
375    }
376
377    /// Get the `action`
378    pub fn action(&self) -> &EntityUID {
379        &self.action
380    }
381
382    /// Get the `context` attributes
383    pub fn context_attrs(&self) -> Option<&Arc<BTreeMap<SmolStr, Value>>> {
384        self.context.as_ref()
385    }
386}
387
388/// A request builder based on a [`PartialRequest`]
389/// Users should use it to iteratively construct a [`Request`] using methods
390/// `add_*`
391#[derive(Debug, Clone)]
392pub struct RequestBuilder<'s> {
393    /// The `PartialRequest`
394    partial_request: PartialRequest,
395    /// Env used for validation
396    schema: &'s ValidatorSchema,
397}
398
399impl<'s> RequestBuilder<'s> {
400    /// Attempt to construct a [`RequestBuilder`] from a [`PartialRequest`] and
401    /// a [`ValidatorSchema`]
402    pub fn new(
403        partial_request: PartialRequest,
404        schema: &'s ValidatorSchema,
405    ) -> Result<Self, RequestBuilderError> {
406        partial_request.validate(schema)?;
407        Ok(Self {
408            partial_request,
409            schema,
410        })
411    }
412
413    /// Attempt to get a concrete [`Request`]
414    /// Return `None` if there are still missing components
415    pub fn get_request(&self) -> Option<Request> {
416        let PartialRequest {
417            principal,
418            action,
419            resource,
420            context,
421        } = &self.partial_request;
422        match (
423            EntityUID::try_from(principal.clone()),
424            EntityUID::try_from(resource.clone()),
425            context,
426        ) {
427            (Ok(principal), Ok(resource), Some(context)) => Some(Request::new_unchecked(
428                principal.into(),
429                action.clone().into(),
430                resource.into(),
431                Some(Context::Value(context.clone())),
432            )),
433            _ => None,
434        }
435    }
436
437    /// Attempt to add `principal`
438    pub fn add_principal(&mut self, candidate: EntityUID) -> Result<(), RequestBuilderError> {
439        self.partial_request
440            .principal
441            .set_candidate(candidate, self.schema)
442            .map_err(|e| e.into_principal_error())
443    }
444
445    /// Attempt to add `resource`
446    pub fn add_resource(&mut self, candidate: EntityUID) -> Result<(), RequestBuilderError> {
447        self.partial_request
448            .resource
449            .set_candidate(candidate, self.schema)
450            .map_err(|e| e.into_resource_error())
451    }
452
453    /// Attempt to add `context`
454    pub fn add_context(&mut self, candidate: &Context) -> Result<(), RequestBuilderError> {
455        if let Context::Value(v) = candidate {
456            if self.partial_request.context.is_some() {
457                Err(RequestBuilderError::ExistingContext)
458            } else {
459                self.schema
460                    .validate_context(
461                        candidate,
462                        &self.partial_request.action,
463                        Extensions::all_available(),
464                    )
465                    .map_err(RequestBuilderError::Validation)?;
466                self.partial_request.context = Some(v.clone());
467                Ok(())
468            }
469        } else {
470            Err(RequestBuilderError::UnknownContextCandidate)
471        }
472    }
473}
474
475#[cfg(test)]
476mod invalid_requests {
477    use std::{collections::BTreeMap, sync::Arc};
478
479    use crate::{
480        ast::Value,
481        extensions::Extensions,
482        test_utils::{expect_err, ExpectedErrorMessage, ExpectedErrorMessageBuilder},
483        tpe::request::PartialRequest,
484        tpe::test_utils::parse_partial_euid,
485        validator::ValidatorSchema,
486    };
487
488    #[track_caller]
489    fn schema() -> ValidatorSchema {
490        ValidatorSchema::from_cedarschema_str(
491            r#"
492        entity A enum ["foo"];
493        entity B;
494        entity C;
495        action a appliesTo {
496          principal: A,
497          resource: B,
498          context: {
499            "" : A,
500          }
501        };
502        action b appliesTo {
503          principal: B,
504          resource: A,
505        };
506        "#,
507            Extensions::all_available(),
508        )
509        .unwrap()
510        .0
511    }
512
513    #[track_caller]
514    fn expect_validation_err(
515        principal: &str,
516        action: &str,
517        resource: &str,
518        context: Option<Arc<BTreeMap<smol_str::SmolStr, Value>>>,
519        msg: &ExpectedErrorMessage<'_>,
520    ) {
521        let err = PartialRequest::new(
522            parse_partial_euid(principal),
523            action.parse().unwrap(),
524            parse_partial_euid(resource),
525            context,
526            &schema(),
527        )
528        .expect_err("should fail to validate");
529        expect_err("", &miette::Report::new(err), msg);
530    }
531
532    #[test]
533    fn unknown_action() {
534        expect_validation_err(
535            "A",
536            r#"Action::"c""#,
537            "B",
538            None,
539            &ExpectedErrorMessageBuilder::error(
540                r#"request's action `Action::"c"` is not declared in the schema"#,
541            )
542            .exactly_one_underline(r#"Action::"c""#)
543            .build(),
544        );
545    }
546
547    #[test]
548    fn unknown_principal() {
549        expect_validation_err(
550            "D",
551            r#"Action::"a""#,
552            "B",
553            None,
554            &ExpectedErrorMessageBuilder::error(
555                r#"principal type `D` is not valid for `Action::"a"`"#,
556            )
557            .help(r#"valid principal types for `Action::"a"`: `A`"#)
558            .exactly_one_underline("D")
559            .build(),
560        );
561    }
562
563    #[test]
564    fn unknown_resource() {
565        expect_validation_err(
566            "A",
567            r#"Action::"a""#,
568            "D",
569            None,
570            &ExpectedErrorMessageBuilder::error(
571                r#"resource type `D` is not valid for `Action::"a"`"#,
572            )
573            .help(r#"valid resource types for `Action::"a"`: `B`"#)
574            .exactly_one_underline("D")
575            .build(),
576        );
577    }
578
579    #[test]
580    fn invalid_principal_for_action() {
581        expect_validation_err(
582            "C",
583            r#"Action::"a""#,
584            "B",
585            None,
586            &ExpectedErrorMessageBuilder::error(
587                r#"principal type `C` is not valid for `Action::"a"`"#,
588            )
589            .help(r#"valid principal types for `Action::"a"`: `A`"#)
590            .exactly_one_underline("C")
591            .build(),
592        );
593    }
594
595    #[test]
596    fn invalid_resource_for_action() {
597        expect_validation_err(
598            "A",
599            r#"Action::"a""#,
600            "C",
601            None,
602            &ExpectedErrorMessageBuilder::error(
603                r#"resource type `C` is not valid for `Action::"a"`"#,
604            )
605            .help(r#"valid resource types for `Action::"a"`: `B`"#)
606            .exactly_one_underline("C")
607            .build(),
608        );
609    }
610
611    #[test]
612    fn invalid_principal_enum() {
613        expect_validation_err(
614            r#"A::"bar""#,
615            r#"Action::"a""#,
616            "B",
617            None,
618            &ExpectedErrorMessageBuilder::error(
619                r#"entity `A::"bar"` is of an enumerated entity type, but `"bar"` is not declared as a valid eid"#,
620            )
621            .help(r#"valid entity eids: "foo""#)
622            .build(),
623        );
624    }
625
626    #[test]
627    fn invalid_resource_enum() {
628        expect_validation_err(
629            "B",
630            r#"Action::"b""#,
631            r#"A::"bar""#,
632            None,
633            &ExpectedErrorMessageBuilder::error(
634                r#"entity `A::"bar"` is of an enumerated entity type, but `"bar"` is not declared as a valid eid"#,
635            )
636            .help(r#"valid entity eids: "foo""#)
637            .build(),
638        );
639    }
640
641    #[test]
642    fn invalid_context() {
643        // action `a` requires a context attribute `""` of type `A`, but we
644        // supply a `Long`
645        expect_validation_err(
646            "A",
647            r#"Action::"a""#,
648            "B",
649            Some(Arc::new(BTreeMap::from_iter([("".into(), 1.into())]))),
650            &ExpectedErrorMessageBuilder::error(
651                r#"context `{"": 1}` is not valid for `Action::"a"`"#,
652            )
653            .build(),
654        );
655    }
656}
657
658#[cfg(test)]
659mod inconsistent_requests {
660    use std::{collections::BTreeMap, sync::Arc};
661
662    use crate::{
663        ast::{Context, EntityUIDEntry, Request, Value},
664        extensions::Extensions,
665        test_utils::{expect_err, ExpectedErrorMessageBuilder},
666        tpe::{request::PartialRequest, test_utils::parse_partial_euid},
667        validator::ValidatorSchema,
668    };
669
670    #[track_caller]
671    fn schema() -> ValidatorSchema {
672        ValidatorSchema::from_cedarschema_str(
673            r#"
674        entity A;
675        entity B;
676        action a appliesTo {
677          principal: A,
678          resource: B,
679          context: {
680            "foo" : Long,
681          }
682        };
683        action b appliesTo {
684          principal: A,
685          resource: B,
686        };
687        "#,
688            Extensions::all_available(),
689        )
690        .unwrap()
691        .0
692    }
693
694    /// A `PartialRequest` with a concrete principal `A::"p"`, action `a`,
695    /// concrete resource `B::"r"`, and context `{foo: 0}`.
696    #[track_caller]
697    fn request() -> PartialRequest {
698        PartialRequest::new(
699            parse_partial_euid(r#"A::"p""#),
700            r#"Action::"a""#.parse().unwrap(),
701            parse_partial_euid(r#"B::"r""#),
702            Some(Arc::new(BTreeMap::from_iter([("foo".into(), 0.into())]))),
703            &schema(),
704        )
705        .unwrap()
706    }
707
708    /// Build a concrete [`Request`] out of the given components.
709    #[track_caller]
710    fn concrete_request(
711        principal: &str,
712        action: &str,
713        resource: &str,
714        context: BTreeMap<smol_str::SmolStr, Value>,
715    ) -> Request {
716        Request::new_unchecked(
717            EntityUIDEntry::known(principal.parse().unwrap(), None),
718            EntityUIDEntry::known(action.parse().unwrap(), None),
719            EntityUIDEntry::known(resource.parse().unwrap(), None),
720            Some(Context::Value(Arc::new(context))),
721        )
722    }
723
724    #[track_caller]
725    fn ctx() -> BTreeMap<smol_str::SmolStr, Value> {
726        BTreeMap::from_iter([("foo".into(), 0.into())])
727    }
728
729    /// Check that [`request()`] is inconsistent with `concrete`, expecting the
730    /// given error message.
731    #[track_caller]
732    fn expect_inconsistency(concrete: &Request, error: &str) {
733        let err = request()
734            .check_consistency(concrete)
735            .expect_err("should be inconsistent");
736        expect_err(
737            "",
738            &miette::Report::new(err),
739            &ExpectedErrorMessageBuilder::error(error).build(),
740        );
741    }
742
743    #[test]
744    fn unknown_principal() {
745        let concrete = Request::new_unchecked(
746            EntityUIDEntry::unknown(),
747            EntityUIDEntry::known(r#"Action::"a""#.parse().unwrap(), None),
748            EntityUIDEntry::known(r#"B::"r""#.parse().unwrap(), None),
749            Some(Context::Value(Arc::new(ctx()))),
750        );
751        expect_inconsistency(&concrete, "the concrete request's principal is unknown");
752    }
753
754    #[test]
755    fn unknown_resource() {
756        let concrete = Request::new_unchecked(
757            EntityUIDEntry::known(r#"A::"p""#.parse().unwrap(), None),
758            EntityUIDEntry::known(r#"Action::"a""#.parse().unwrap(), None),
759            EntityUIDEntry::unknown(),
760            Some(Context::Value(Arc::new(ctx()))),
761        );
762        expect_inconsistency(&concrete, "the concrete request's resource is unknown");
763    }
764
765    #[test]
766    fn unknown_action() {
767        let concrete = Request::new_unchecked(
768            EntityUIDEntry::known(r#"A::"p""#.parse().unwrap(), None),
769            EntityUIDEntry::unknown(),
770            EntityUIDEntry::known(r#"B::"r""#.parse().unwrap(), None),
771            Some(Context::Value(Arc::new(ctx()))),
772        );
773        expect_inconsistency(&concrete, "the concrete request's action is unknown");
774    }
775
776    #[test]
777    fn unknown_context() {
778        let concrete = Request::new_unchecked(
779            EntityUIDEntry::known(r#"A::"p""#.parse().unwrap(), None),
780            EntityUIDEntry::known(r#"Action::"a""#.parse().unwrap(), None),
781            EntityUIDEntry::known(r#"B::"r""#.parse().unwrap(), None),
782            None,
783        );
784        expect_inconsistency(&concrete, "the concrete request's context is unknown");
785    }
786
787    #[test]
788    fn principal_type() {
789        let concrete = concrete_request(r#"B::"p""#, r#"Action::"a""#, r#"B::"r""#, ctx());
790        expect_inconsistency(
791            &concrete,
792            "partial request principal type `A` does not match concrete request principal type `B`",
793        );
794    }
795
796    #[test]
797    fn principal_id() {
798        let concrete = concrete_request(r#"A::"other""#, r#"Action::"a""#, r#"B::"r""#, ctx());
799        expect_inconsistency(
800            &concrete,
801            "partial request principal id `p` does not match concrete request principal id `other`",
802        );
803    }
804
805    #[test]
806    fn action_type() {
807        let concrete = concrete_request(r#"A::"p""#, r#"Foo::"a""#, r#"B::"r""#, ctx());
808        expect_inconsistency(
809            &concrete,
810            r#"partial request action `Action::"a"` does not match concrete request action `Foo::"a"`"#,
811        );
812    }
813
814    #[test]
815    fn action_id() {
816        let concrete = concrete_request(r#"A::"p""#, r#"Action::"b""#, r#"B::"r""#, ctx());
817        expect_inconsistency(
818            &concrete,
819            r#"partial request action `Action::"a"` does not match concrete request action `Action::"b"`"#,
820        );
821    }
822
823    #[test]
824    fn resource_type() {
825        let concrete = concrete_request(r#"A::"p""#, r#"Action::"a""#, r#"A::"r""#, ctx());
826        expect_inconsistency(
827            &concrete,
828            "partial request resource type `B` does not match concrete request resource type `A`",
829        );
830    }
831
832    #[test]
833    fn resource_id() {
834        let concrete = concrete_request(r#"A::"p""#, r#"Action::"a""#, r#"B::"other""#, ctx());
835        expect_inconsistency(
836            &concrete,
837            "partial request resource id `r` does not match concrete request resource id `other`",
838        );
839    }
840
841    #[test]
842    fn context() {
843        let concrete = concrete_request(
844            r#"A::"p""#,
845            r#"Action::"a""#,
846            r#"B::"r""#,
847            BTreeMap::from_iter([("foo".into(), 1.into())]),
848        );
849        expect_inconsistency(
850            &concrete,
851            "the partial and concrete request contexts do not match",
852        );
853    }
854
855    #[test]
856    fn concrete_context_contains_unknowns() {
857        use crate::ast::{Expr, Unknown};
858        let residual =
859            BTreeMap::from_iter([("foo".into(), Expr::unknown(Unknown::new_untyped("foo")))]);
860        let concrete = Request::new_unchecked(
861            EntityUIDEntry::known(r#"A::"p""#.parse().unwrap(), None),
862            EntityUIDEntry::known(r#"Action::"a""#.parse().unwrap(), None),
863            EntityUIDEntry::known(r#"B::"r""#.parse().unwrap(), None),
864            Some(Context::RestrictedResidual(Arc::new(residual))),
865        );
866        expect_inconsistency(
867            &concrete,
868            "the concrete request's context contains unknowns",
869        );
870    }
871}
872
873#[cfg(test)]
874mod request_builder_tests {
875    use std::{collections::BTreeMap, sync::Arc};
876
877    use cool_asserts::assert_matches;
878    use std::str::FromStr;
879
880    use crate::{
881        ast::{Context, EntityUID},
882        extensions::Extensions,
883        tpe::{
884            err::RequestBuilderError,
885            request::{PartialRequest, RequestBuilder},
886            test_utils::parse_partial_euid,
887        },
888        validator::{RequestValidationError, ValidatorSchema},
889    };
890
891    #[track_caller]
892    fn schema() -> ValidatorSchema {
893        ValidatorSchema::from_cedarschema_str(
894            r#"
895        entity A enum ["foo"];
896        entity B;
897        action a appliesTo {
898          principal: A,
899          resource: B,
900          context: {
901            "" : A,
902          }
903        };
904        "#,
905            Extensions::all_available(),
906        )
907        .unwrap()
908        .0
909    }
910
911    #[track_caller]
912    fn request() -> PartialRequest {
913        PartialRequest::new(
914            parse_partial_euid("A"),
915            r#"Action::"a""#.parse().unwrap(),
916            parse_partial_euid("B"),
917            None,
918            &schema(),
919        )
920        .unwrap()
921    }
922
923    #[test]
924    fn build() {
925        let schema = schema();
926        let request = request();
927        let mut builder = RequestBuilder::new(request, &schema).expect("should succeed");
928
929        // add principal of incorrect type
930        assert_matches!(
931            builder.add_principal(r#"B::"""#.parse().unwrap()),
932            Err(RequestBuilderError::IncorrectPrincipalEntityType(_))
933        );
934        // add invalid principal
935        assert_matches!(
936            builder.add_principal(r#"A::"""#.parse().unwrap()),
937            Err(RequestBuilderError::Validation(
938                RequestValidationError::InvalidEnumEntity(_)
939            )),
940        );
941        // add a principal
942        assert_matches!(builder.add_principal(r#"A::"foo""#.parse().unwrap()), Ok(_));
943        // then we can't add it again
944        assert_matches!(
945            builder.add_principal(r#"A::"foo""#.parse().unwrap()),
946            Err(RequestBuilderError::ExistingPrincipal(_))
947        );
948        // and we're not done
949        assert_matches!(builder.get_request(), None);
950        // add resource
951        assert_matches!(builder.add_resource(r#"B::"foo""#.parse().unwrap()), Ok(_));
952        // so we can't do it again
953        assert_matches!(
954            builder.add_resource(r#"B::"foo""#.parse().unwrap()),
955            Err(RequestBuilderError::ExistingResource(_))
956        );
957        // add a context of incorrect type
958        assert_matches!(
959            builder.add_context(&Context::Value(Arc::new(BTreeMap::from_iter([(
960                "".into(),
961                1.into()
962            )])))),
963            Err(RequestBuilderError::Validation(
964                RequestValidationError::InvalidContext(_)
965            ))
966        );
967        // add a context
968        assert_matches!(
969            builder.add_context(&Context::Value(Arc::new(BTreeMap::from_iter([(
970                "".into(),
971                EntityUID::from_str(r#"A::"foo""#).unwrap().into(),
972            )])))),
973            Ok(_)
974        );
975        // can't do it again
976        assert_matches!(
977            builder.add_context(&Context::Value(Arc::new(BTreeMap::from_iter([(
978                "".into(),
979                EntityUID::from_str(r#"A::"foo""#).unwrap().into(),
980            )])))),
981            Err(RequestBuilderError::ExistingContext)
982        );
983        // and we're done
984        assert_matches!(builder.get_request(), Some(_));
985    }
986}