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::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/// Partial EntityUID
43#[derive(Debug, Clone)]
44pub struct PartialEntityUID {
45    /// Typename of the entity
46    pub ty: EntityType,
47    /// Optional EID of the entity
48    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        // Entity type must be declared
117        let entity_ty = schema
118            .get_entity_type(&self.ty)
119            .ok_or_else(|| PartialEUIDValidationError::UndeclaredType(self.ty.clone()))?;
120        // If we have a concrete uid and this is an enum entity, it must be one
121        // of the choices
122        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/// Represents the request tuple <P, A, R, C> (see the Cedar design doc).
174#[derive(Debug, Clone)]
175pub struct PartialRequest {
176    /// Principal associated with the request
177    principal: PartialEntityUID,
178
179    /// Action associated with the request
180    action: EntityUID,
181
182    /// Resource associated with the request
183    resource: PartialEntityUID,
184
185    /// Context associated with the request.
186    /// `None` means that variable will result in a residual for partial evaluation.
187    context: Option<Arc<BTreeMap<SmolStr, Value>>>,
188}
189
190impl PartialRequest {
191    /// Create a well-formed `PartialRequest` (i.e., it conforms to the schema)
192    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    // Find the matching `RequestEnv`
210    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    // Validate `self` with `schema`
229    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    /// Check consistency between a [`PartialRequest`] and a [`Request`]
256    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    /// Get the [`EntityType`] of `principal`
298    pub fn principal_type(&self) -> &EntityType {
299        &self.principal.ty
300    }
301
302    /// Get the [`EntityType`] of `resource`
303    pub fn resource_type(&self) -> &EntityType {
304        &self.resource.ty
305    }
306
307    /// Get the `principal`
308    pub fn principal(&self) -> &PartialEntityUID {
309        &self.principal
310    }
311
312    /// Get the `resource`
313    pub fn resource(&self) -> &PartialEntityUID {
314        &self.resource
315    }
316
317    /// Get the `action`
318    pub fn action(&self) -> &EntityUID {
319        &self.action
320    }
321
322    /// Get the `context` attributes
323    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        // action `a` requires a context attribute `""` of type `A`, but we
497        // supply a `Long`
498        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    /// A `PartialRequest` with a concrete principal `A::"p"`, action `a`,
548    /// concrete resource `B::"r"`, and context `{foo: 0}`.
549    #[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    /// Build a concrete [`Request`] out of the given components.
562    #[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    /// Check that [`request()`] is inconsistent with `concrete`, expecting the
583    /// given error message.
584    #[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}