1use std::{collections::BTreeMap, sync::Arc};
20
21use crate::ast::{EntityUIDEntry, RequestSchema};
22use crate::entities::conformance::err::InvalidEnumEntityError;
23use crate::tpe::err::{
24 InconsistentActionError, InconsistentPrincipalEidError, InconsistentPrincipalTypeError,
25 InconsistentResourceEidError, InconsistentResourceTypeError, NoMatchingReqEnvError,
26 RequestConsistencyError,
27};
28use crate::validator::request_validation_errors::{
29 UndeclaredActionError, UndeclaredPrincipalTypeError, UndeclaredResourceTypeError,
30};
31use crate::validator::{
32 types::RequestEnv, RequestValidationError, ValidationMode, ValidatorEntityTypeKind,
33 ValidatorSchema,
34};
35use crate::{
36 ast::{Context, Eid, EntityType, EntityUID, Request, Value},
37 entities::conformance::is_valid_enumerated_entity,
38 extensions::Extensions,
39};
40use smol_str::SmolStr;
41
42#[derive(Debug, Clone)]
44pub struct PartialEntityUID {
45 pub ty: EntityType,
47 pub eid: Option<Eid>,
49}
50
51#[derive(Debug)]
52enum PartialEUIDConsistencyError {
53 Unknown,
54 InconsistentType(EntityType, EntityType),
55 InconsistentEid(Eid, Eid),
56}
57
58impl PartialEUIDConsistencyError {
59 pub fn into_resource_error(self) -> RequestConsistencyError {
60 match self {
61 PartialEUIDConsistencyError::Unknown => RequestConsistencyError::UnknownResource,
62 PartialEUIDConsistencyError::InconsistentType(partial, concrete) => {
63 InconsistentResourceTypeError { partial, concrete }.into()
64 }
65 PartialEUIDConsistencyError::InconsistentEid(partial, concrete) => {
66 InconsistentResourceEidError { partial, concrete }.into()
67 }
68 }
69 }
70
71 pub fn into_principal_error(self) -> RequestConsistencyError {
72 match self {
73 PartialEUIDConsistencyError::Unknown => RequestConsistencyError::UnknownPrincipal,
74 PartialEUIDConsistencyError::InconsistentType(partial, concrete) => {
75 InconsistentPrincipalTypeError { partial, concrete }.into()
76 }
77 PartialEUIDConsistencyError::InconsistentEid(partial, concrete) => {
78 InconsistentPrincipalEidError { partial, concrete }.into()
79 }
80 }
81 }
82}
83
84#[derive(Debug)]
85enum PartialEUIDValidationError {
86 UndeclaredType(EntityType),
87 InvalidEnum(InvalidEnumEntityError),
88}
89
90impl PartialEUIDValidationError {
91 pub fn into_resource_error(self) -> RequestValidationError {
92 match self {
93 PartialEUIDValidationError::UndeclaredType(resource_ty) => {
94 UndeclaredResourceTypeError { resource_ty }.into()
95 }
96 PartialEUIDValidationError::InvalidEnum(enum_err) => enum_err.into(),
97 }
98 }
99
100 pub fn into_principal_error(self) -> RequestValidationError {
101 match self {
102 PartialEUIDValidationError::UndeclaredType(principal_ty) => {
103 UndeclaredPrincipalTypeError { principal_ty }.into()
104 }
105 PartialEUIDValidationError::InvalidEnum(enum_err) => enum_err.into(),
106 }
107 }
108}
109
110impl PartialEntityUID {
111 fn check_type(
112 &self,
113 schema: &ValidatorSchema,
114 uid: Option<&EntityUID>,
115 ) -> Result<(), PartialEUIDValidationError> {
116 let entity_ty = schema
118 .get_entity_type(&self.ty)
119 .ok_or_else(|| PartialEUIDValidationError::UndeclaredType(self.ty.clone()))?;
120 if let (ValidatorEntityTypeKind::Enum(choices), Some(uid)) = (&entity_ty.kind, uid) {
123 is_valid_enumerated_entity(choices, uid)
124 .map_err(PartialEUIDValidationError::InvalidEnum)?;
125 }
126 Ok(())
127 }
128
129 fn validate(&self, schema: &ValidatorSchema) -> Result<(), PartialEUIDValidationError> {
130 self.check_type(schema, EntityUID::try_from(self.clone()).ok().as_ref())
131 }
132
133 fn check_consistency(&self, entry: &EntityUIDEntry) -> Result<(), PartialEUIDConsistencyError> {
134 let EntityUIDEntry::Known { euid, .. } = entry else {
135 return Err(PartialEUIDConsistencyError::Unknown);
136 };
137 if euid.entity_type() != &self.ty {
138 return Err(PartialEUIDConsistencyError::InconsistentType(
139 self.ty.clone(),
140 euid.entity_type().clone(),
141 ));
142 }
143 if let Some(eid) = &self.eid {
144 if eid != euid.eid() {
145 return Err(PartialEUIDConsistencyError::InconsistentEid(
146 eid.clone(),
147 euid.eid().clone(),
148 ));
149 }
150 }
151 Ok(())
152 }
153}
154
155impl TryFrom<PartialEntityUID> for EntityUID {
156 type Error = ();
157 fn try_from(value: PartialEntityUID) -> Result<EntityUID, ()> {
158 if let Some(eid) = value.eid {
159 Ok(EntityUID::from_components(value.ty, eid, None))
160 } else {
161 Err(())
162 }
163 }
164}
165
166impl From<EntityUID> for PartialEntityUID {
167 fn from(value: EntityUID) -> Self {
168 let (ty, eid) = value.components();
169 Self { ty, eid: Some(eid) }
170 }
171}
172
173#[derive(Debug, Clone)]
175pub struct PartialRequest {
176 principal: PartialEntityUID,
178
179 action: EntityUID,
181
182 resource: PartialEntityUID,
184
185 context: Option<Arc<BTreeMap<SmolStr, Value>>>,
188}
189
190impl PartialRequest {
191 pub fn new(
193 principal: PartialEntityUID,
194 action: EntityUID,
195 resource: PartialEntityUID,
196 context: Option<Arc<BTreeMap<SmolStr, Value>>>,
197 schema: &ValidatorSchema,
198 ) -> Result<Self, RequestValidationError> {
199 let req = Self {
200 principal,
201 action,
202 resource,
203 context,
204 };
205 req.validate(schema)?;
206 Ok(req)
207 }
208
209 pub(crate) fn find_request_env<'s>(
211 &self,
212 schema: &'s ValidatorSchema,
213 ) -> Result<RequestEnv<'s>, NoMatchingReqEnvError> {
214 #[expect(
215 clippy::unwrap_used,
216 reason = "strict validation should produce concrete action entity uid"
217 )]
218 schema
219 .unlinked_request_envs(ValidationMode::Strict)
220 .find(|env| {
221 env.action_entity_uid().unwrap() == &self.action
222 && env.principal_entity_type() == Some(&self.principal.ty)
223 && env.resource_entity_type() == Some(&self.resource.ty)
224 })
225 .ok_or(NoMatchingReqEnvError)
226 }
227
228 pub(crate) fn validate(&self, schema: &ValidatorSchema) -> Result<(), RequestValidationError> {
230 if let Some(action_id) = schema.get_action_id(&self.action) {
231 action_id.check_principal_type(&self.principal.ty, &self.action.clone().into())?;
232 action_id.check_resource_type(&self.resource.ty, &self.action.clone().into())?;
233 self.principal
234 .validate(schema)
235 .map_err(|e| e.into_principal_error())?;
236 self.resource
237 .validate(schema)
238 .map_err(|e| e.into_resource_error())?;
239 if let Some(m) = &self.context {
240 schema.validate_context(
241 &Context::Value(m.clone()),
242 &self.action,
243 Extensions::all_available(),
244 )?;
245 }
246 Ok(())
247 } else {
248 Err(UndeclaredActionError {
249 action: self.action.clone().into(),
250 }
251 .into())
252 }
253 }
254
255 pub fn check_consistency(&self, request: &Request) -> Result<(), RequestConsistencyError> {
257 self.principal
258 .check_consistency(&request.principal)
259 .map_err(|e| e.into_principal_error())?;
260 self.resource
261 .check_consistency(&request.resource)
262 .map_err(|e| e.into_resource_error())?;
263
264 match &request.action {
265 EntityUIDEntry::Unknown { .. } => {
266 return Err(RequestConsistencyError::UnknownAction);
267 }
268 EntityUIDEntry::Known { euid, .. } => {
269 if euid.as_ref() != &self.action {
270 return Err(InconsistentActionError {
271 partial: self.action.clone(),
272 concrete: euid.as_ref().clone(),
273 }
274 .into());
275 }
276 }
277 }
278
279 match &request.context {
280 Some(Context::Value(c)) => {
281 if let Some(m) = &self.context {
282 if c != m {
283 return Err(RequestConsistencyError::InconsistentContext);
284 }
285 }
286 }
287 Some(Context::RestrictedResidual { .. }) => {
288 return Err(RequestConsistencyError::ConcreteContextContainsUnknowns);
289 }
290 None => {
291 return Err(RequestConsistencyError::UnknownContext);
292 }
293 }
294 Ok(())
295 }
296
297 pub fn principal_type(&self) -> &EntityType {
299 &self.principal.ty
300 }
301
302 pub fn resource_type(&self) -> &EntityType {
304 &self.resource.ty
305 }
306
307 pub fn principal(&self) -> &PartialEntityUID {
309 &self.principal
310 }
311
312 pub fn resource(&self) -> &PartialEntityUID {
314 &self.resource
315 }
316
317 pub fn action(&self) -> &EntityUID {
319 &self.action
320 }
321
322 pub fn context_attrs(&self) -> Option<&Arc<BTreeMap<SmolStr, Value>>> {
324 self.context.as_ref()
325 }
326}
327
328#[cfg(test)]
329mod invalid_requests {
330 use std::{collections::BTreeMap, sync::Arc};
331
332 use crate::{
333 ast::Value,
334 extensions::Extensions,
335 test_utils::{expect_err, ExpectedErrorMessage, ExpectedErrorMessageBuilder},
336 tpe::request::PartialRequest,
337 tpe::test_utils::parse_partial_euid,
338 validator::ValidatorSchema,
339 };
340
341 #[track_caller]
342 fn schema() -> ValidatorSchema {
343 ValidatorSchema::from_cedarschema_str(
344 r#"
345 entity A enum ["foo"];
346 entity B;
347 entity C;
348 action a appliesTo {
349 principal: A,
350 resource: B,
351 context: {
352 "" : A,
353 }
354 };
355 action b appliesTo {
356 principal: B,
357 resource: A,
358 };
359 "#,
360 Extensions::all_available(),
361 )
362 .unwrap()
363 .0
364 }
365
366 #[track_caller]
367 fn expect_validation_err(
368 principal: &str,
369 action: &str,
370 resource: &str,
371 context: Option<Arc<BTreeMap<smol_str::SmolStr, Value>>>,
372 msg: &ExpectedErrorMessage<'_>,
373 ) {
374 let err = PartialRequest::new(
375 parse_partial_euid(principal),
376 action.parse().unwrap(),
377 parse_partial_euid(resource),
378 context,
379 &schema(),
380 )
381 .expect_err("should fail to validate");
382 expect_err("", &miette::Report::new(err), msg);
383 }
384
385 #[test]
386 fn unknown_action() {
387 expect_validation_err(
388 "A",
389 r#"Action::"c""#,
390 "B",
391 None,
392 &ExpectedErrorMessageBuilder::error(
393 r#"request's action `Action::"c"` is not declared in the schema"#,
394 )
395 .exactly_one_underline(r#"Action::"c""#)
396 .build(),
397 );
398 }
399
400 #[test]
401 fn unknown_principal() {
402 expect_validation_err(
403 "D",
404 r#"Action::"a""#,
405 "B",
406 None,
407 &ExpectedErrorMessageBuilder::error(
408 r#"principal type `D` is not valid for `Action::"a"`"#,
409 )
410 .help(r#"valid principal types for `Action::"a"`: `A`"#)
411 .exactly_one_underline("D")
412 .build(),
413 );
414 }
415
416 #[test]
417 fn unknown_resource() {
418 expect_validation_err(
419 "A",
420 r#"Action::"a""#,
421 "D",
422 None,
423 &ExpectedErrorMessageBuilder::error(
424 r#"resource type `D` is not valid for `Action::"a"`"#,
425 )
426 .help(r#"valid resource types for `Action::"a"`: `B`"#)
427 .exactly_one_underline("D")
428 .build(),
429 );
430 }
431
432 #[test]
433 fn invalid_principal_for_action() {
434 expect_validation_err(
435 "C",
436 r#"Action::"a""#,
437 "B",
438 None,
439 &ExpectedErrorMessageBuilder::error(
440 r#"principal type `C` is not valid for `Action::"a"`"#,
441 )
442 .help(r#"valid principal types for `Action::"a"`: `A`"#)
443 .exactly_one_underline("C")
444 .build(),
445 );
446 }
447
448 #[test]
449 fn invalid_resource_for_action() {
450 expect_validation_err(
451 "A",
452 r#"Action::"a""#,
453 "C",
454 None,
455 &ExpectedErrorMessageBuilder::error(
456 r#"resource type `C` is not valid for `Action::"a"`"#,
457 )
458 .help(r#"valid resource types for `Action::"a"`: `B`"#)
459 .exactly_one_underline("C")
460 .build(),
461 );
462 }
463
464 #[test]
465 fn invalid_principal_enum() {
466 expect_validation_err(
467 r#"A::"bar""#,
468 r#"Action::"a""#,
469 "B",
470 None,
471 &ExpectedErrorMessageBuilder::error(
472 r#"entity `A::"bar"` is of an enumerated entity type, but `"bar"` is not declared as a valid eid"#,
473 )
474 .help(r#"valid entity eids: "foo""#)
475 .build(),
476 );
477 }
478
479 #[test]
480 fn invalid_resource_enum() {
481 expect_validation_err(
482 "B",
483 r#"Action::"b""#,
484 r#"A::"bar""#,
485 None,
486 &ExpectedErrorMessageBuilder::error(
487 r#"entity `A::"bar"` is of an enumerated entity type, but `"bar"` is not declared as a valid eid"#,
488 )
489 .help(r#"valid entity eids: "foo""#)
490 .build(),
491 );
492 }
493
494 #[test]
495 fn invalid_context() {
496 expect_validation_err(
499 "A",
500 r#"Action::"a""#,
501 "B",
502 Some(Arc::new(BTreeMap::from_iter([("".into(), 1.into())]))),
503 &ExpectedErrorMessageBuilder::error(
504 r#"context `{"": 1}` is not valid for `Action::"a"`"#,
505 )
506 .build(),
507 );
508 }
509}
510
511#[cfg(test)]
512mod inconsistent_requests {
513 use std::{collections::BTreeMap, sync::Arc};
514
515 use crate::{
516 ast::{Context, EntityUIDEntry, Request, Value},
517 extensions::Extensions,
518 test_utils::{expect_err, ExpectedErrorMessageBuilder},
519 tpe::{request::PartialRequest, test_utils::parse_partial_euid},
520 validator::ValidatorSchema,
521 };
522
523 #[track_caller]
524 fn schema() -> ValidatorSchema {
525 ValidatorSchema::from_cedarschema_str(
526 r#"
527 entity A;
528 entity B;
529 action a appliesTo {
530 principal: A,
531 resource: B,
532 context: {
533 "foo" : Long,
534 }
535 };
536 action b appliesTo {
537 principal: A,
538 resource: B,
539 };
540 "#,
541 Extensions::all_available(),
542 )
543 .unwrap()
544 .0
545 }
546
547 #[track_caller]
550 fn request() -> PartialRequest {
551 PartialRequest::new(
552 parse_partial_euid(r#"A::"p""#),
553 r#"Action::"a""#.parse().unwrap(),
554 parse_partial_euid(r#"B::"r""#),
555 Some(Arc::new(BTreeMap::from_iter([("foo".into(), 0.into())]))),
556 &schema(),
557 )
558 .unwrap()
559 }
560
561 #[track_caller]
563 fn concrete_request(
564 principal: &str,
565 action: &str,
566 resource: &str,
567 context: BTreeMap<smol_str::SmolStr, Value>,
568 ) -> Request {
569 Request::new_unchecked(
570 EntityUIDEntry::known(principal.parse().unwrap(), None),
571 EntityUIDEntry::known(action.parse().unwrap(), None),
572 EntityUIDEntry::known(resource.parse().unwrap(), None),
573 Some(Context::Value(Arc::new(context))),
574 )
575 }
576
577 #[track_caller]
578 fn ctx() -> BTreeMap<smol_str::SmolStr, Value> {
579 BTreeMap::from_iter([("foo".into(), 0.into())])
580 }
581
582 #[track_caller]
585 fn expect_inconsistency(concrete: &Request, error: &str) {
586 let err = request()
587 .check_consistency(concrete)
588 .expect_err("should be inconsistent");
589 expect_err(
590 "",
591 &miette::Report::new(err),
592 &ExpectedErrorMessageBuilder::error(error).build(),
593 );
594 }
595
596 #[test]
597 fn unknown_principal() {
598 let concrete = Request::new_unchecked(
599 EntityUIDEntry::unknown(),
600 EntityUIDEntry::known(r#"Action::"a""#.parse().unwrap(), None),
601 EntityUIDEntry::known(r#"B::"r""#.parse().unwrap(), None),
602 Some(Context::Value(Arc::new(ctx()))),
603 );
604 expect_inconsistency(&concrete, "the concrete request's principal is unknown");
605 }
606
607 #[test]
608 fn unknown_resource() {
609 let concrete = Request::new_unchecked(
610 EntityUIDEntry::known(r#"A::"p""#.parse().unwrap(), None),
611 EntityUIDEntry::known(r#"Action::"a""#.parse().unwrap(), None),
612 EntityUIDEntry::unknown(),
613 Some(Context::Value(Arc::new(ctx()))),
614 );
615 expect_inconsistency(&concrete, "the concrete request's resource is unknown");
616 }
617
618 #[test]
619 fn unknown_action() {
620 let concrete = Request::new_unchecked(
621 EntityUIDEntry::known(r#"A::"p""#.parse().unwrap(), None),
622 EntityUIDEntry::unknown(),
623 EntityUIDEntry::known(r#"B::"r""#.parse().unwrap(), None),
624 Some(Context::Value(Arc::new(ctx()))),
625 );
626 expect_inconsistency(&concrete, "the concrete request's action is unknown");
627 }
628
629 #[test]
630 fn unknown_context() {
631 let concrete = Request::new_unchecked(
632 EntityUIDEntry::known(r#"A::"p""#.parse().unwrap(), None),
633 EntityUIDEntry::known(r#"Action::"a""#.parse().unwrap(), None),
634 EntityUIDEntry::known(r#"B::"r""#.parse().unwrap(), None),
635 None,
636 );
637 expect_inconsistency(&concrete, "the concrete request's context is unknown");
638 }
639
640 #[test]
641 fn principal_type() {
642 let concrete = concrete_request(r#"B::"p""#, r#"Action::"a""#, r#"B::"r""#, ctx());
643 expect_inconsistency(
644 &concrete,
645 "partial request principal type `A` does not match concrete request principal type `B`",
646 );
647 }
648
649 #[test]
650 fn principal_id() {
651 let concrete = concrete_request(r#"A::"other""#, r#"Action::"a""#, r#"B::"r""#, ctx());
652 expect_inconsistency(
653 &concrete,
654 "partial request principal id `p` does not match concrete request principal id `other`",
655 );
656 }
657
658 #[test]
659 fn action_type() {
660 let concrete = concrete_request(r#"A::"p""#, r#"Foo::"a""#, r#"B::"r""#, ctx());
661 expect_inconsistency(
662 &concrete,
663 r#"partial request action `Action::"a"` does not match concrete request action `Foo::"a"`"#,
664 );
665 }
666
667 #[test]
668 fn action_id() {
669 let concrete = concrete_request(r#"A::"p""#, r#"Action::"b""#, r#"B::"r""#, ctx());
670 expect_inconsistency(
671 &concrete,
672 r#"partial request action `Action::"a"` does not match concrete request action `Action::"b"`"#,
673 );
674 }
675
676 #[test]
677 fn resource_type() {
678 let concrete = concrete_request(r#"A::"p""#, r#"Action::"a""#, r#"A::"r""#, ctx());
679 expect_inconsistency(
680 &concrete,
681 "partial request resource type `B` does not match concrete request resource type `A`",
682 );
683 }
684
685 #[test]
686 fn resource_id() {
687 let concrete = concrete_request(r#"A::"p""#, r#"Action::"a""#, r#"B::"other""#, ctx());
688 expect_inconsistency(
689 &concrete,
690 "partial request resource id `r` does not match concrete request resource id `other`",
691 );
692 }
693
694 #[test]
695 fn context() {
696 let concrete = concrete_request(
697 r#"A::"p""#,
698 r#"Action::"a""#,
699 r#"B::"r""#,
700 BTreeMap::from_iter([("foo".into(), 1.into())]),
701 );
702 expect_inconsistency(
703 &concrete,
704 "the partial and concrete request contexts do not match",
705 );
706 }
707
708 #[test]
709 fn concrete_context_contains_unknowns() {
710 use crate::ast::{Expr, Unknown};
711 let residual =
712 BTreeMap::from_iter([("foo".into(), Expr::unknown(Unknown::new_untyped("foo")))]);
713 let concrete = Request::new_unchecked(
714 EntityUIDEntry::known(r#"A::"p""#.parse().unwrap(), None),
715 EntityUIDEntry::known(r#"Action::"a""#.parse().unwrap(), None),
716 EntityUIDEntry::known(r#"B::"r""#.parse().unwrap(), None),
717 Some(Context::RestrictedResidual(Arc::new(residual))),
718 );
719 expect_inconsistency(
720 &concrete,
721 "the concrete request's context contains unknowns",
722 );
723 }
724}