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(choices, &uid)?;
159 }
160 }
161 } else {
162 return Err(UndeclaredPrincipalTypeError {
163 principal_ty: self.principal.ty.clone(),
164 }
165 .into());
166 }
167 if let Some(resource_ty) = schema.get_entity_type(&self.resource.ty) {
168 if let std::result::Result::Ok(uid) = self.resource.clone().try_into() {
169 if let ValidatorEntityType {
170 kind: ValidatorEntityTypeKind::Enum(choices),
171 ..
172 } = resource_ty
173 {
174 is_valid_enumerated_entity(choices, &uid)?;
175 }
176 }
177 } else {
178 return Err(UndeclaredResourceTypeError {
179 resource_ty: self.resource.ty.clone(),
180 }
181 .into());
182 }
183 if let Some(m) = &self.context {
184 schema.validate_context(
185 &Context::Value(m.clone()),
186 &self.action,
187 Extensions::all_available(),
188 )?;
189 }
190 Ok(())
191 } else {
192 Err(UndeclaredActionError {
193 action: self.action.clone().into(),
194 }
195 .into())
196 }
197 }
198
199 pub fn check_consistency(
201 &self,
202 request: &Request,
203 ) -> std::result::Result<(), RequestConsistencyError> {
204 match &request.principal {
205 EntityUIDEntry::Unknown { .. } => {
206 return Err(RequestConsistencyError::UnknownPrincipal);
207 }
208 EntityUIDEntry::Known { euid, .. } => {
209 if euid.entity_type() != &self.principal.ty {
210 return Err(InconsistentPrincipalTypeError {
211 partial: self.principal.ty.clone(),
212 concrete: euid.entity_type().clone(),
213 }
214 .into());
215 }
216 if let Some(eid) = &self.principal.eid {
217 if eid != euid.eid() {
218 return Err(InconsistentPrincipalEidError {
219 partial: eid.clone(),
220 concrete: euid.eid().clone(),
221 }
222 .into());
223 }
224 }
225 }
226 }
227
228 match &request.resource {
229 EntityUIDEntry::Unknown { .. } => {
230 return Err(RequestConsistencyError::UnknownResource);
231 }
232 EntityUIDEntry::Known { euid, .. } => {
233 if euid.entity_type() != &self.resource.ty {
234 return Err(InconsistentResourceTypeError {
235 partial: self.resource.ty.clone(),
236 concrete: euid.entity_type().clone(),
237 }
238 .into());
239 }
240 if let Some(eid) = &self.resource.eid {
241 if eid != euid.eid() {
242 return Err(InconsistentResourceEidError {
243 partial: eid.clone(),
244 concrete: euid.eid().clone(),
245 }
246 .into());
247 }
248 }
249 }
250 }
251
252 match &request.action {
253 EntityUIDEntry::Unknown { .. } => {
254 return Err(RequestConsistencyError::UnknownAction);
255 }
256 EntityUIDEntry::Known { euid, .. } => {
257 if euid.as_ref() != &self.action {
258 return Err(InconsistentActionError {
259 partial: self.action.clone(),
260 concrete: euid.as_ref().clone(),
261 }
262 .into());
263 }
264 }
265 }
266
267 match &request.context {
268 Some(Context::Value(c)) => {
269 if let Some(m) = &self.context {
270 if c != m {
271 return Err(RequestConsistencyError::InconsistentContext);
272 }
273 }
274 }
275 Some(Context::RestrictedResidual { .. }) => {
276 return Err(RequestConsistencyError::ConcreteContextContainsUnknowns);
277 }
278 None => {
279 return Err(RequestConsistencyError::UnknownContext);
280 }
281 }
282 Ok(())
283 }
284
285 pub fn get_principal_type(&self) -> EntityType {
287 self.principal.ty.clone()
288 }
289
290 pub fn get_resource_type(&self) -> EntityType {
292 self.resource.ty.clone()
293 }
294
295 pub fn get_principal(&self) -> PartialEntityUID {
297 self.principal.clone()
298 }
299
300 pub fn get_resource(&self) -> PartialEntityUID {
302 self.resource.clone()
303 }
304
305 pub fn get_action(&self) -> EntityUID {
307 self.action.clone()
308 }
309
310 pub fn get_context_attrs(&self) -> Option<&BTreeMap<SmolStr, Value>> {
312 self.context.as_ref().map(|attrs| attrs.as_ref())
313 }
314}
315
316#[derive(Debug, Clone)]
320pub struct RequestBuilder<'s> {
321 partial_request: PartialRequest,
323 schema: &'s ValidatorSchema,
325}
326
327impl<'s> RequestBuilder<'s> {
328 pub fn new(
331 partial_request: PartialRequest,
332 schema: &'s ValidatorSchema,
333 ) -> std::result::Result<Self, RequestBuilderError> {
334 partial_request.validate(schema)?;
335 Ok(Self {
336 partial_request,
337 schema,
338 })
339 }
340
341 pub fn get_request(&self) -> Option<Request> {
344 let PartialRequest {
345 principal,
346 action,
347 resource,
348 context,
349 } = &self.partial_request;
350 match (
351 EntityUID::try_from(principal.clone()),
352 EntityUID::try_from(resource.clone()),
353 context,
354 ) {
355 (
356 std::result::Result::Ok(principal),
357 std::result::Result::Ok(resource),
358 Some(context),
359 ) => Some(Request::new_unchecked(
360 principal.into(),
361 action.clone().into(),
362 resource.into(),
363 Some(Context::Value(context.clone())),
364 )),
365 _ => None,
366 }
367 }
368
369 pub fn add_principal(
371 &mut self,
372 candidate: &EntityUID,
373 ) -> std::result::Result<(), RequestBuilderError> {
374 if let PartialEntityUID { eid: Some(eid), .. } = &self.partial_request.principal {
375 Err(ExistingPrincipalError {
376 principal: EntityUID::from_components(
377 self.partial_request.principal.ty.clone(),
378 eid.clone(),
379 None,
380 ),
381 }
382 .into())
383 } else {
384 #[expect(
385 clippy::unwrap_used,
386 reason = "partial_request is validated and hence the entity type must exist in the schema"
387 )]
388 if candidate.entity_type() != &self.partial_request.principal.ty {
389 Err(IncorrectPrincipalEntityTypeError {
390 ty: candidate.entity_type().clone(),
391 expected: self.partial_request.principal.ty.clone(),
392 }
393 .into())
394 } else {
395 let principal_ty = self
396 .schema
397 .get_entity_type(&self.partial_request.principal.ty)
398 .unwrap();
399 if let ValidatorEntityType {
400 kind: ValidatorEntityTypeKind::Enum(choices),
401 ..
402 } = principal_ty
403 {
404 is_valid_enumerated_entity(choices, candidate)
405 .map_err(RequestBuilderError::InvalidPrincipalCandidate)?;
406 }
407 self.partial_request.principal = PartialEntityUID {
408 ty: candidate.entity_type().clone(),
409 eid: Some(candidate.eid().clone()),
410 };
411 Ok(())
412 }
413 }
414 }
415
416 pub fn add_resource(
418 &mut self,
419 candidate: &EntityUID,
420 ) -> std::result::Result<(), RequestBuilderError> {
421 if let PartialEntityUID { eid: Some(eid), .. } = &self.partial_request.resource {
422 Err(ExistingResourceError {
423 resource: EntityUID::from_components(
424 self.partial_request.resource.ty.clone(),
425 eid.clone(),
426 None,
427 ),
428 }
429 .into())
430 } else {
431 #[expect(
432 clippy::unwrap_used,
433 reason = "partial_request is validated and hence the entity type must exist in the schema"
434 )]
435 if candidate.entity_type() != &self.partial_request.resource.ty {
436 Err(IncorrectResourceEntityTypeError {
437 ty: candidate.entity_type().clone(),
438 expected: self.partial_request.resource.ty.clone(),
439 }
440 .into())
441 } else {
442 let resource_ty = self
443 .schema
444 .get_entity_type(&self.partial_request.resource.ty)
445 .unwrap();
446 if let ValidatorEntityType {
447 kind: ValidatorEntityTypeKind::Enum(choices),
448 ..
449 } = resource_ty
450 {
451 is_valid_enumerated_entity(choices, candidate)
452 .map_err(RequestBuilderError::InvalidResourceCandidate)?;
453 }
454 self.partial_request.resource = PartialEntityUID {
455 ty: candidate.entity_type().clone(),
456 eid: Some(candidate.eid().clone()),
457 };
458 Ok(())
459 }
460 }
461 }
462
463 pub fn add_context(
465 &mut self,
466 candidate: &Context,
467 ) -> std::result::Result<(), RequestBuilderError> {
468 if let Context::Value(v) = candidate {
469 if self.partial_request.context.is_some() {
470 Err(RequestBuilderError::ExistingContext)
471 } else {
472 self.schema
473 .validate_context(
474 candidate,
475 &self.partial_request.action,
476 Extensions::all_available(),
477 )
478 .map_err(RequestBuilderError::IllTypedContextCandidate)?;
479 self.partial_request.context = Some(v.clone());
480 Ok(())
481 }
482 } else {
483 Err(RequestBuilderError::UnknownContextCandidate)
484 }
485 }
486}
487
488#[cfg(test)]
489mod request_builder_tests {
490 use std::{collections::BTreeMap, sync::Arc};
491
492 use cool_asserts::assert_matches;
493 use std::str::FromStr;
494
495 use crate::{
496 ast::{Context, EntityUID},
497 extensions::Extensions,
498 tpe::{
499 err::RequestBuilderError,
500 request::{PartialEntityUID, PartialRequest, RequestBuilder},
501 },
502 validator::ValidatorSchema,
503 };
504
505 #[track_caller]
506 fn schema() -> ValidatorSchema {
507 ValidatorSchema::from_cedarschema_str(
508 r#"
509 entity A enum ["foo"];
510 entity B;
511 action a appliesTo {
512 principal: A,
513 resource: B,
514 context: {
515 "" : A,
516 }
517 };
518 "#,
519 Extensions::all_available(),
520 )
521 .unwrap()
522 .0
523 }
524
525 #[track_caller]
526 fn request() -> PartialRequest {
527 PartialRequest::new(
528 PartialEntityUID {
529 ty: "A".parse().unwrap(),
530 eid: None,
531 },
532 r#"Action::"a""#.parse().unwrap(),
533 PartialEntityUID {
534 ty: "B".parse().unwrap(),
535 eid: None,
536 },
537 None,
538 &schema(),
539 )
540 .unwrap()
541 }
542
543 #[test]
544 fn build() {
545 let schema = schema();
546 let request = request();
547 let mut builder = RequestBuilder::new(request, &schema).expect("should succeed");
548
549 assert_matches!(
551 builder.add_principal(&r#"B::"""#.parse().unwrap()),
552 Err(RequestBuilderError::IncorrectPrincipalEntityType(_))
553 );
554 assert_matches!(
556 builder.add_principal(&r#"A::"""#.parse().unwrap()),
557 Err(RequestBuilderError::InvalidPrincipalCandidate(_))
558 );
559 assert_matches!(
561 builder.add_principal(&r#"A::"foo""#.parse().unwrap()),
562 Ok(_)
563 );
564 assert_matches!(
566 builder.add_principal(&r#"A::"foo""#.parse().unwrap()),
567 Err(RequestBuilderError::ExistingPrincipal(_))
568 );
569 assert_matches!(builder.get_request(), None);
571 assert_matches!(builder.add_resource(&r#"B::"foo""#.parse().unwrap()), Ok(_));
573 assert_matches!(
575 builder.add_resource(&r#"B::"foo""#.parse().unwrap()),
576 Err(RequestBuilderError::ExistingResource(_))
577 );
578 assert_matches!(
580 builder.add_context(&Context::Value(Arc::new(BTreeMap::from_iter([(
581 "".into(),
582 1.into()
583 )])))),
584 Err(RequestBuilderError::IllTypedContextCandidate(_))
585 );
586 assert_matches!(
588 builder.add_context(&Context::Value(Arc::new(BTreeMap::from_iter([(
589 "".into(),
590 EntityUID::from_str(r#"A::"foo""#).unwrap().into(),
591 )])))),
592 Ok(_)
593 );
594 assert_matches!(
596 builder.add_context(&Context::Value(Arc::new(BTreeMap::from_iter([(
597 "".into(),
598 EntityUID::from_str(r#"A::"foo""#).unwrap().into(),
599 )])))),
600 Err(RequestBuilderError::ExistingContext)
601 );
602 assert_matches!(builder.get_request(), Some(_));
604 }
605}