1use 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#[derive(Debug, Clone)]
46pub struct PartialEntityUID {
47 pub ty: EntityType,
49 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 let entity_ty = schema
153 .get_entity_type(&self.ty)
154 .ok_or_else(|| PartialEUIDValidationError::UndeclaredType(self.ty.clone()))?;
155 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 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#[derive(Debug, Clone)]
235pub struct PartialRequest {
236 principal: PartialEntityUID,
238
239 action: EntityUID,
241
242 resource: PartialEntityUID,
244
245 context: Option<Arc<BTreeMap<SmolStr, Value>>>,
248}
249
250impl PartialRequest {
251 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 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 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 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 pub fn principal_type(&self) -> &EntityType {
359 &self.principal.ty
360 }
361
362 pub fn resource_type(&self) -> &EntityType {
364 &self.resource.ty
365 }
366
367 pub fn principal(&self) -> &PartialEntityUID {
369 &self.principal
370 }
371
372 pub fn resource(&self) -> &PartialEntityUID {
374 &self.resource
375 }
376
377 pub fn action(&self) -> &EntityUID {
379 &self.action
380 }
381
382 pub fn context_attrs(&self) -> Option<&Arc<BTreeMap<SmolStr, Value>>> {
384 self.context.as_ref()
385 }
386}
387
388#[derive(Debug, Clone)]
392pub struct RequestBuilder<'s> {
393 partial_request: PartialRequest,
395 schema: &'s ValidatorSchema,
397}
398
399impl<'s> RequestBuilder<'s> {
400 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 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 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 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 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 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 #[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 #[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 #[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 assert_matches!(
931 builder.add_principal(r#"B::"""#.parse().unwrap()),
932 Err(RequestBuilderError::IncorrectPrincipalEntityType(_))
933 );
934 assert_matches!(
936 builder.add_principal(r#"A::"""#.parse().unwrap()),
937 Err(RequestBuilderError::Validation(
938 RequestValidationError::InvalidEnumEntity(_)
939 )),
940 );
941 assert_matches!(builder.add_principal(r#"A::"foo""#.parse().unwrap()), Ok(_));
943 assert_matches!(
945 builder.add_principal(r#"A::"foo""#.parse().unwrap()),
946 Err(RequestBuilderError::ExistingPrincipal(_))
947 );
948 assert_matches!(builder.get_request(), None);
950 assert_matches!(builder.add_resource(r#"B::"foo""#.parse().unwrap()), Ok(_));
952 assert_matches!(
954 builder.add_resource(r#"B::"foo""#.parse().unwrap()),
955 Err(RequestBuilderError::ExistingResource(_))
956 );
957 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 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 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 assert_matches!(builder.get_request(), Some(_));
985 }
986}