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(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    /// Check consistency between a [`PartialRequest`] and a [`Request`]
200    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    /// Get the [`EntityType`] of `principal`
286    pub fn get_principal_type(&self) -> EntityType {
287        self.principal.ty.clone()
288    }
289
290    /// Get the [`EntityType`] of `resource`
291    pub fn get_resource_type(&self) -> EntityType {
292        self.resource.ty.clone()
293    }
294
295    /// Get the `principal`
296    pub fn get_principal(&self) -> PartialEntityUID {
297        self.principal.clone()
298    }
299
300    /// Get the `resource`
301    pub fn get_resource(&self) -> PartialEntityUID {
302        self.resource.clone()
303    }
304
305    /// Get the `action`
306    pub fn get_action(&self) -> EntityUID {
307        self.action.clone()
308    }
309
310    /// Get the `context` attributes
311    pub fn get_context_attrs(&self) -> Option<&BTreeMap<SmolStr, Value>> {
312        self.context.as_ref().map(|attrs| attrs.as_ref())
313    }
314}
315
316/// A request builder based on a [`PartialRequest`]
317/// Users should use it to iteratively construct a [`Request`] using methods
318/// `add_*`
319#[derive(Debug, Clone)]
320pub struct RequestBuilder<'s> {
321    /// The `PartialRequest`
322    partial_request: PartialRequest,
323    /// Env used for validation
324    schema: &'s ValidatorSchema,
325}
326
327impl<'s> RequestBuilder<'s> {
328    /// Attempt to construct a [`RequestBuilder`] from a [`PartialRequest`] and
329    /// a [`ValidatorSchema`]
330    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    /// Attempt to get a concrete [`Request`]
342    /// Return `None` if there are still missing components
343    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    /// Attempt to add `principal`
370    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    /// Attempt to add `resource`
417    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    /// Attempt to add `context`
464    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        // add principal of incorrect type
550        assert_matches!(
551            builder.add_principal(&r#"B::"""#.parse().unwrap()),
552            Err(RequestBuilderError::IncorrectPrincipalEntityType(_))
553        );
554        // add invalid principal
555        assert_matches!(
556            builder.add_principal(&r#"A::"""#.parse().unwrap()),
557            Err(RequestBuilderError::InvalidPrincipalCandidate(_))
558        );
559        // add a principal
560        assert_matches!(
561            builder.add_principal(&r#"A::"foo""#.parse().unwrap()),
562            Ok(_)
563        );
564        // then we can't add it again
565        assert_matches!(
566            builder.add_principal(&r#"A::"foo""#.parse().unwrap()),
567            Err(RequestBuilderError::ExistingPrincipal(_))
568        );
569        // and we're not done
570        assert_matches!(builder.get_request(), None);
571        // add resource
572        assert_matches!(builder.add_resource(&r#"B::"foo""#.parse().unwrap()), Ok(_));
573        // so we can't do it again
574        assert_matches!(
575            builder.add_resource(&r#"B::"foo""#.parse().unwrap()),
576            Err(RequestBuilderError::ExistingResource(_))
577        );
578        // add a context of incorrect type
579        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        // add a context
587        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        // can't do it again
595        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        // and we're done
603        assert_matches!(builder.get_request(), Some(_));
604    }
605}