1use std::{collections::BTreeMap, sync::Arc};
20
21use crate::ast::{EntityUIDEntry, RequestSchema};
22use crate::tpe::err::{
23 ExistingPrincipalError, ExistingResourceError, InconsistentActionError,
24 InconsistentPrincipalEidError, InconsistentPrincipalTypeError, InconsistentResourceEidError,
25 InconsistentResourceTypeError, IncorrectPrincipalEntityTypeError,
26 IncorrectResourceEntityTypeError, NoMatchingReqEnvError, RequestBuilderError,
27 RequestConsistencyError,
28};
29use crate::validator::request_validation_errors::{
30 UndeclaredActionError, UndeclaredPrincipalTypeError, UndeclaredResourceTypeError,
31};
32use crate::validator::{
33 types::RequestEnv, RequestValidationError, ValidationMode, ValidatorEntityType,
34 ValidatorEntityTypeKind, ValidatorSchema,
35};
36use crate::{
37 ast::{Context, Eid, EntityType, EntityUID, Request, Value},
38 entities::conformance::is_valid_enumerated_entity,
39 extensions::Extensions,
40};
41use smol_str::SmolStr;
42
43#[derive(Debug, Clone)]
45pub struct PartialEntityUID {
46 pub ty: EntityType,
48 pub eid: Option<Eid>,
50}
51
52impl TryFrom<PartialEntityUID> for EntityUID {
53 type Error = ();
54 fn try_from(value: PartialEntityUID) -> std::result::Result<EntityUID, ()> {
55 if let Some(eid) = value.eid {
56 std::result::Result::Ok(EntityUID::from_components(value.ty, eid, None))
57 } else {
58 Err(())
59 }
60 }
61}
62
63impl From<EntityUID> for PartialEntityUID {
64 fn from(value: EntityUID) -> Self {
65 Self {
66 ty: value.entity_type().clone(),
67 eid: Some(value.eid().clone()),
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct PartialRequest {
75 pub(crate) principal: PartialEntityUID,
77
78 pub(crate) action: EntityUID,
80
81 pub(crate) resource: PartialEntityUID,
83
84 pub(crate) context: Option<Arc<BTreeMap<SmolStr, Value>>>,
87}
88
89impl PartialRequest {
90 pub fn new(
92 principal: PartialEntityUID,
93 action: EntityUID,
94 resource: PartialEntityUID,
95
96 context: Option<Arc<BTreeMap<SmolStr, Value>>>,
97 schema: &ValidatorSchema,
98 ) -> std::result::Result<Self, RequestValidationError> {
99 let req = Self {
100 principal,
101 action,
102 resource,
103 context,
104 };
105 req.validate(schema)?;
106 Ok(req)
107 }
108
109 pub fn new_unchecked(
111 principal: PartialEntityUID,
112 resource: PartialEntityUID,
113 action: EntityUID,
114 context: Option<Arc<BTreeMap<SmolStr, Value>>>,
115 ) -> Self {
116 Self {
117 principal,
118 action,
119 resource,
120 context,
121 }
122 }
123
124 pub(crate) fn find_request_env<'s>(
126 &self,
127 schema: &'s ValidatorSchema,
128 ) -> std::result::Result<RequestEnv<'s>, NoMatchingReqEnvError> {
129 #[expect(
130 clippy::unwrap_used,
131 reason = "strict validation should produce concrete action entity uid"
132 )]
133 schema
134 .unlinked_request_envs(ValidationMode::Strict)
135 .find(|env| {
136 env.action_entity_uid().unwrap() == &self.action
137 && env.principal_entity_type() == Some(&self.principal.ty)
138 && env.resource_entity_type() == Some(&self.resource.ty)
139 })
140 .ok_or(NoMatchingReqEnvError)
141 }
142
143 pub(crate) fn validate(
145 &self,
146 schema: &ValidatorSchema,
147 ) -> std::result::Result<(), RequestValidationError> {
148 if let Some(action_id) = schema.get_action_id(&self.action) {
149 action_id.check_principal_type(&self.principal.ty, &self.action.clone().into())?;
150 action_id.check_resource_type(&self.resource.ty, &self.action.clone().into())?;
151 if let Some(principal_ty) = schema.get_entity_type(&self.principal.ty) {
152 if let std::result::Result::Ok(uid) = self.principal.clone().try_into() {
153 if let ValidatorEntityType {
154 kind: ValidatorEntityTypeKind::Enum(choices),
155 ..
156 } = principal_ty
157 {
158 is_valid_enumerated_entity(
159 &Vec::from(choices.clone().map(Eid::new)),
160 &uid,
161 )?;
162 }
163 }
164 } else {
165 return Err(UndeclaredPrincipalTypeError {
166 principal_ty: self.principal.ty.clone(),
167 }
168 .into());
169 }
170 if let Some(resource_ty) = schema.get_entity_type(&self.resource.ty) {
171 if let std::result::Result::Ok(uid) = self.resource.clone().try_into() {
172 if let ValidatorEntityType {
173 kind: ValidatorEntityTypeKind::Enum(choices),
174 ..
175 } = resource_ty
176 {
177 is_valid_enumerated_entity(
178 &Vec::from(choices.clone().map(Eid::new)),
179 &uid,
180 )?;
181 }
182 }
183 } else {
184 return Err(UndeclaredResourceTypeError {
185 resource_ty: self.resource.ty.clone(),
186 }
187 .into());
188 }
189 if let Some(m) = &self.context {
190 schema.validate_context(
191 &Context::Value(m.clone()),
192 &self.action,
193 Extensions::all_available(),
194 )?;
195 }
196 Ok(())
197 } else {
198 Err(UndeclaredActionError {
199 action: self.action.clone().into(),
200 }
201 .into())
202 }
203 }
204
205 pub fn check_consistency(
207 &self,
208 request: &Request,
209 ) -> std::result::Result<(), RequestConsistencyError> {
210 match &request.principal {
211 EntityUIDEntry::Unknown { .. } => {
212 return Err(RequestConsistencyError::UnknownPrincipal);
213 }
214 EntityUIDEntry::Known { euid, .. } => {
215 if euid.entity_type() != &self.principal.ty {
216 return Err(InconsistentPrincipalTypeError {
217 partial: self.principal.ty.clone(),
218 concrete: euid.entity_type().clone(),
219 }
220 .into());
221 }
222 if let Some(eid) = &self.principal.eid {
223 if eid != euid.eid() {
224 return Err(InconsistentPrincipalEidError {
225 partial: eid.clone(),
226 concrete: euid.eid().clone(),
227 }
228 .into());
229 }
230 }
231 }
232 }
233
234 match &request.resource {
235 EntityUIDEntry::Unknown { .. } => {
236 return Err(RequestConsistencyError::UnknownResource);
237 }
238 EntityUIDEntry::Known { euid, .. } => {
239 if euid.entity_type() != &self.resource.ty {
240 return Err(InconsistentResourceTypeError {
241 partial: self.resource.ty.clone(),
242 concrete: euid.entity_type().clone(),
243 }
244 .into());
245 }
246 if let Some(eid) = &self.resource.eid {
247 if eid != euid.eid() {
248 return Err(InconsistentResourceEidError {
249 partial: eid.clone(),
250 concrete: euid.eid().clone(),
251 }
252 .into());
253 }
254 }
255 }
256 }
257
258 match &request.action {
259 EntityUIDEntry::Unknown { .. } => {
260 return Err(RequestConsistencyError::UnknownAction);
261 }
262 EntityUIDEntry::Known { euid, .. } => {
263 if euid.as_ref() != &self.action {
264 return Err(InconsistentActionError {
265 partial: self.action.clone(),
266 concrete: euid.as_ref().clone(),
267 }
268 .into());
269 }
270 }
271 }
272
273 match &request.context {
274 Some(Context::Value(c)) => {
275 if let Some(m) = &self.context {
276 if c != m {
277 return Err(RequestConsistencyError::InconsistentContext);
278 }
279 }
280 }
281 Some(Context::RestrictedResidual { .. }) => {
282 return Err(RequestConsistencyError::ConcreteContextContainsUnknowns);
283 }
284 None => {
285 return Err(RequestConsistencyError::UnknownContext);
286 }
287 }
288 Ok(())
289 }
290
291 pub fn get_principal_type(&self) -> EntityType {
293 self.principal.ty.clone()
294 }
295
296 pub fn get_resource_type(&self) -> EntityType {
298 self.resource.ty.clone()
299 }
300
301 pub fn get_principal(&self) -> PartialEntityUID {
303 self.principal.clone()
304 }
305
306 pub fn get_resource(&self) -> PartialEntityUID {
308 self.resource.clone()
309 }
310
311 pub fn get_action(&self) -> EntityUID {
313 self.action.clone()
314 }
315
316 pub fn get_context_attrs(&self) -> Option<&BTreeMap<SmolStr, Value>> {
318 self.context.as_ref().map(|attrs| attrs.as_ref())
319 }
320}
321
322#[derive(Debug, Clone)]
326pub struct RequestBuilder<'s> {
327 partial_request: PartialRequest,
329 schema: &'s ValidatorSchema,
331}
332
333impl<'s> RequestBuilder<'s> {
334 pub fn new(
337 partial_request: PartialRequest,
338 schema: &'s ValidatorSchema,
339 ) -> std::result::Result<Self, RequestBuilderError> {
340 partial_request.validate(schema)?;
341 Ok(Self {
342 partial_request,
343 schema,
344 })
345 }
346
347 pub fn get_request(&self) -> Option<Request> {
350 let PartialRequest {
351 principal,
352 action,
353 resource,
354 context,
355 } = &self.partial_request;
356 match (
357 EntityUID::try_from(principal.clone()),
358 EntityUID::try_from(resource.clone()),
359 context,
360 ) {
361 (
362 std::result::Result::Ok(principal),
363 std::result::Result::Ok(resource),
364 Some(context),
365 ) => Some(Request::new_unchecked(
366 principal.into(),
367 action.clone().into(),
368 resource.into(),
369 Some(Context::Value(context.clone())),
370 )),
371 _ => None,
372 }
373 }
374
375 pub fn add_principal(
377 &mut self,
378 candidate: &EntityUID,
379 ) -> std::result::Result<(), RequestBuilderError> {
380 if let PartialEntityUID { eid: Some(eid), .. } = &self.partial_request.principal {
381 Err(ExistingPrincipalError {
382 principal: EntityUID::from_components(
383 self.partial_request.principal.ty.clone(),
384 eid.clone(),
385 None,
386 ),
387 }
388 .into())
389 } else {
390 #[expect(
391 clippy::unwrap_used,
392 reason = "partial_request is validated and hence the entity type must exist in the schema"
393 )]
394 if candidate.entity_type() != &self.partial_request.principal.ty {
395 Err(IncorrectPrincipalEntityTypeError {
396 ty: candidate.entity_type().clone(),
397 expected: self.partial_request.principal.ty.clone(),
398 }
399 .into())
400 } else {
401 let principal_ty = self
402 .schema
403 .get_entity_type(&self.partial_request.principal.ty)
404 .unwrap();
405 if let ValidatorEntityType {
406 kind: ValidatorEntityTypeKind::Enum(choices),
407 ..
408 } = principal_ty
409 {
410 is_valid_enumerated_entity(
411 &Vec::from(choices.clone().map(Eid::new)),
412 candidate,
413 )
414 .map_err(RequestBuilderError::InvalidPrincipalCandidate)?;
415 }
416 self.partial_request.principal = PartialEntityUID {
417 ty: candidate.entity_type().clone(),
418 eid: Some(candidate.eid().clone()),
419 };
420 Ok(())
421 }
422 }
423 }
424
425 pub fn add_resource(
427 &mut self,
428 candidate: &EntityUID,
429 ) -> std::result::Result<(), RequestBuilderError> {
430 if let PartialEntityUID { eid: Some(eid), .. } = &self.partial_request.resource {
431 Err(ExistingResourceError {
432 resource: EntityUID::from_components(
433 self.partial_request.resource.ty.clone(),
434 eid.clone(),
435 None,
436 ),
437 }
438 .into())
439 } else {
440 #[expect(
441 clippy::unwrap_used,
442 reason = "partial_request is validated and hence the entity type must exist in the schema"
443 )]
444 if candidate.entity_type() != &self.partial_request.resource.ty {
445 Err(IncorrectResourceEntityTypeError {
446 ty: candidate.entity_type().clone(),
447 expected: self.partial_request.resource.ty.clone(),
448 }
449 .into())
450 } else {
451 let resource_ty = self
452 .schema
453 .get_entity_type(&self.partial_request.resource.ty)
454 .unwrap();
455 if let ValidatorEntityType {
456 kind: ValidatorEntityTypeKind::Enum(choices),
457 ..
458 } = resource_ty
459 {
460 is_valid_enumerated_entity(
461 &Vec::from(choices.clone().map(Eid::new)),
462 candidate,
463 )
464 .map_err(RequestBuilderError::InvalidResourceCandidate)?;
465 }
466 self.partial_request.resource = PartialEntityUID {
467 ty: candidate.entity_type().clone(),
468 eid: Some(candidate.eid().clone()),
469 };
470 Ok(())
471 }
472 }
473 }
474
475 pub fn add_context(
477 &mut self,
478 candidate: &Context,
479 ) -> std::result::Result<(), RequestBuilderError> {
480 if let Context::Value(v) = candidate {
481 if self.partial_request.context.is_some() {
482 Err(RequestBuilderError::ExistingContext)
483 } else {
484 self.schema
485 .validate_context(
486 candidate,
487 &self.partial_request.action,
488 Extensions::all_available(),
489 )
490 .map_err(RequestBuilderError::IllTypedContextCandidate)?;
491 self.partial_request.context = Some(v.clone());
492 Ok(())
493 }
494 } else {
495 Err(RequestBuilderError::UnknownContextCandidate)
496 }
497 }
498}
499
500#[cfg(test)]
501mod request_builder_tests {
502 use std::{collections::BTreeMap, sync::Arc};
503
504 use cool_asserts::assert_matches;
505 use std::str::FromStr;
506
507 use crate::{
508 ast::{Context, EntityUID},
509 extensions::Extensions,
510 tpe::{
511 err::RequestBuilderError,
512 request::{PartialEntityUID, PartialRequest, RequestBuilder},
513 },
514 validator::ValidatorSchema,
515 };
516
517 #[track_caller]
518 fn schema() -> ValidatorSchema {
519 ValidatorSchema::from_cedarschema_str(
520 r#"
521 entity A enum ["foo"];
522 entity B;
523 action a appliesTo {
524 principal: A,
525 resource: B,
526 context: {
527 "" : A,
528 }
529 };
530 "#,
531 Extensions::all_available(),
532 )
533 .unwrap()
534 .0
535 }
536
537 #[track_caller]
538 fn request() -> PartialRequest {
539 PartialRequest::new(
540 PartialEntityUID {
541 ty: "A".parse().unwrap(),
542 eid: None,
543 },
544 r#"Action::"a""#.parse().unwrap(),
545 PartialEntityUID {
546 ty: "B".parse().unwrap(),
547 eid: None,
548 },
549 None,
550 &schema(),
551 )
552 .unwrap()
553 }
554
555 #[test]
556 fn build() {
557 let schema = schema();
558 let request = request();
559 let mut builder = RequestBuilder::new(request, &schema).expect("should succeed");
560
561 assert_matches!(
563 builder.add_principal(&r#"B::"""#.parse().unwrap()),
564 Err(RequestBuilderError::IncorrectPrincipalEntityType(_))
565 );
566 assert_matches!(
568 builder.add_principal(&r#"A::"""#.parse().unwrap()),
569 Err(RequestBuilderError::InvalidPrincipalCandidate(_))
570 );
571 assert_matches!(
573 builder.add_principal(&r#"A::"foo""#.parse().unwrap()),
574 Ok(_)
575 );
576 assert_matches!(
578 builder.add_principal(&r#"A::"foo""#.parse().unwrap()),
579 Err(RequestBuilderError::ExistingPrincipal(_))
580 );
581 assert_matches!(builder.get_request(), None);
583 assert_matches!(builder.add_resource(&r#"B::"foo""#.parse().unwrap()), Ok(_));
585 assert_matches!(
587 builder.add_resource(&r#"B::"foo""#.parse().unwrap()),
588 Err(RequestBuilderError::ExistingResource(_))
589 );
590 assert_matches!(
592 builder.add_context(&Context::Value(Arc::new(BTreeMap::from_iter([(
593 "".into(),
594 1.into()
595 )])))),
596 Err(RequestBuilderError::IllTypedContextCandidate(_))
597 );
598 assert_matches!(
600 builder.add_context(&Context::Value(Arc::new(BTreeMap::from_iter([(
601 "".into(),
602 EntityUID::from_str(r#"A::"foo""#).unwrap().into(),
603 )])))),
604 Ok(_)
605 );
606 assert_matches!(
608 builder.add_context(&Context::Value(Arc::new(BTreeMap::from_iter([(
609 "".into(),
610 EntityUID::from_str(r#"A::"foo""#).unwrap().into(),
611 )])))),
612 Err(RequestBuilderError::ExistingContext)
613 );
614 assert_matches!(builder.get_request(), Some(_));
616 }
617}