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::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/// Partial EntityUID
44#[derive(Debug, Clone)]
45pub struct PartialEntityUID {
46    /// Typename of the entity
47    pub ty: EntityType,
48    /// Optional EID of the entity
49    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/// Represents the request tuple <P, A, R, C> (see the Cedar design doc).
73#[derive(Debug, Clone)]
74pub struct PartialRequest {
75    /// Principal associated with the request
76    pub(crate) principal: PartialEntityUID,
77
78    /// Action associated with the request
79    pub(crate) action: EntityUID,
80
81    /// Resource associated with the request
82    pub(crate) resource: PartialEntityUID,
83
84    /// Context associated with the request.
85    /// `None` means that variable will result in a residual for partial evaluation.
86    pub(crate) context: Option<Arc<BTreeMap<SmolStr, Value>>>,
87}
88
89impl PartialRequest {
90    /// Create a well-formed `PartialRequest` (i.e., it conforms to the schema)
91    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    /// Like `new` but do not perform any validation
110    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    // Find the matching `RequestEnv`
125    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    // Validate `self` with `schema`
144    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    /// Check consistency between a [`PartialRequest`] and a [`Request`]
206    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    /// Get the [`EntityType`] of `principal`
292    pub fn get_principal_type(&self) -> EntityType {
293        self.principal.ty.clone()
294    }
295
296    /// Get the [`EntityType`] of `resource`
297    pub fn get_resource_type(&self) -> EntityType {
298        self.resource.ty.clone()
299    }
300
301    /// Get the `principal`
302    pub fn get_principal(&self) -> PartialEntityUID {
303        self.principal.clone()
304    }
305
306    /// Get the `resource`
307    pub fn get_resource(&self) -> PartialEntityUID {
308        self.resource.clone()
309    }
310
311    /// Get the `action`
312    pub fn get_action(&self) -> EntityUID {
313        self.action.clone()
314    }
315
316    /// Get the `context` attributes
317    pub fn get_context_attrs(&self) -> Option<&BTreeMap<SmolStr, Value>> {
318        self.context.as_ref().map(|attrs| attrs.as_ref())
319    }
320}
321
322/// A request builder based on a [`PartialRequest`]
323/// Users should use it to iteratively construct a [`Request`] using methods
324/// `add_*`
325#[derive(Debug, Clone)]
326pub struct RequestBuilder<'s> {
327    /// The `PartialRequest`
328    partial_request: PartialRequest,
329    /// Env used for validation
330    schema: &'s ValidatorSchema,
331}
332
333impl<'s> RequestBuilder<'s> {
334    /// Attempt to construct a [`RequestBuilder`] from a [`PartialRequest`] and
335    /// a [`ValidatorSchema`]
336    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    /// Attempt to get a concrete [`Request`]
348    /// Return `None` if there are still missing components
349    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    /// Attempt to add `principal`
376    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    /// Attempt to add `resource`
426    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    /// Attempt to add `context`
476    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        // add principal of incorrect type
562        assert_matches!(
563            builder.add_principal(&r#"B::"""#.parse().unwrap()),
564            Err(RequestBuilderError::IncorrectPrincipalEntityType(_))
565        );
566        // add invalid principal
567        assert_matches!(
568            builder.add_principal(&r#"A::"""#.parse().unwrap()),
569            Err(RequestBuilderError::InvalidPrincipalCandidate(_))
570        );
571        // add a principal
572        assert_matches!(
573            builder.add_principal(&r#"A::"foo""#.parse().unwrap()),
574            Ok(_)
575        );
576        // then we can't add it again
577        assert_matches!(
578            builder.add_principal(&r#"A::"foo""#.parse().unwrap()),
579            Err(RequestBuilderError::ExistingPrincipal(_))
580        );
581        // and we're not done
582        assert_matches!(builder.get_request(), None);
583        // add resource
584        assert_matches!(builder.add_resource(&r#"B::"foo""#.parse().unwrap()), Ok(_));
585        // so we can't do it again
586        assert_matches!(
587            builder.add_resource(&r#"B::"foo""#.parse().unwrap()),
588            Err(RequestBuilderError::ExistingResource(_))
589        );
590        // add a context of incorrect type
591        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        // add a context
599        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        // can't do it again
607        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        // and we're done
615        assert_matches!(builder.get_request(), Some(_));
616    }
617}