Skip to main content

cedar_policy/proto/
api.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
17use crate::proto::entities::entities_model_to_api;
18
19use super::super::api;
20use super::{ast::ProtobufConversionError, models, traits};
21use prost::Message as _;
22use traits::TryValidate as _;
23
24/// Macro that implements `From<A>` and `TryFrom<B>` for types where
25/// one conversion direction is infallible, the other is not. This is typically the case where
26/// the API type converts to protobuf models without failing, but converting the protobuf model
27/// to the API type requires additional checks.
28macro_rules! fallible_conversions {
29    ( $A:ty, $A_expr:expr, $B:ty ) => {
30        impl From<&$A> for $B {
31            fn from(v: &$A) -> $B {
32                Self::from(&v.0)
33            }
34        }
35
36        impl TryFrom<$B> for $A {
37            type Error = ProtobufConversionError;
38            fn try_from(v: $B) -> Result<$A, Self::Error> {
39                Ok($A_expr(v.try_into()?))
40            }
41        }
42    };
43}
44
45// fallible conversions (encode infallible, decode fallible)
46
47fallible_conversions!(api::Entity, api::Entity, models::Entity);
48fallible_conversions!(api::EntityUid, api::EntityUid, models::EntityUid);
49fallible_conversions!(api::Entities, api::Entities, models::Entities);
50fallible_conversions!(api::Schema, api::Schema, models::Schema);
51fallible_conversions!(api::EntityTypeName, api::EntityTypeName, models::Name);
52fallible_conversions!(api::EntityNamespace, api::EntityNamespace, models::Name);
53fallible_conversions!(api::Expression, api::Expression, models::Expr);
54fallible_conversions!(api::Request, api::Request, models::Request);
55
56// nonstandard conversions
57
58impl From<&api::Template> for models::TemplateBody {
59    fn from(v: &api::Template) -> Self {
60        Self::from(&v.ast)
61    }
62}
63
64impl TryFrom<models::TemplateBody> for api::Template {
65    type Error = ProtobufConversionError;
66    fn try_from(v: models::TemplateBody) -> Result<Self, Self::Error> {
67        Ok(Self::from_ast(v.try_into()?))
68    }
69}
70
71impl From<&api::Policy> for models::Policy {
72    fn from(v: &api::Policy) -> Self {
73        Self::from(&v.ast)
74    }
75}
76
77impl From<&api::PolicySet> for models::PolicySet {
78    fn from(v: &api::PolicySet) -> Self {
79        Self::from(&v.ast)
80    }
81}
82
83impl TryFrom<models::PolicySet> for api::PolicySet {
84    type Error = ProtobufConversionError;
85    fn try_from(v: models::PolicySet) -> Result<Self, Self::Error> {
86        let ast: cedar_policy_core::ast::PolicySet = v.try_into()?;
87        Ok(Self::from_ast(ast))
88    }
89}
90
91/// Macro that implements `traits::Protobuf` for cases where `From<>` and `TryFrom<>`
92/// conversions exist between the api type `$api` and the protobuf model type `$model`
93macro_rules! standard_protobuf_impl {
94    ( $api:ty, $model:ty) => {
95        impl traits::Protobuf for $api {
96            fn encode(&self) -> Result<Vec<u8>, traits::EncodeError> {
97                traits::encode_to_vec::<$model, _>(self)
98            }
99            fn decode_unchecked(buf: impl prost::bytes::Buf) -> Result<Self, traits::DecodeError> {
100                traits::try_decode::<$model, _, _>(buf)
101            }
102        }
103    };
104}
105
106// standard implementations of `traits::Protobuf`
107
108standard_protobuf_impl!(api::Entity, models::Entity);
109standard_protobuf_impl!(api::Schema, models::Schema);
110standard_protobuf_impl!(api::EntityTypeName, models::Name);
111standard_protobuf_impl!(api::EntityNamespace, models::Name);
112standard_protobuf_impl!(api::Template, models::TemplateBody);
113standard_protobuf_impl!(api::Expression, models::Expr);
114standard_protobuf_impl!(api::Request, models::Request);
115
116// nonstandard implementations of `traits::Protobuf`
117
118impl traits::Protobuf for api::Entities {
119    fn encode(&self) -> Result<Vec<u8>, traits::EncodeError> {
120        traits::encode_to_vec::<models::Entities, _>(self)
121    }
122    fn decode(buf: impl prost::bytes::Buf) -> Result<Self, traits::DecodeError> {
123        // Uses the standard TryFrom path which computes TC via ComputeNow
124        let entities: Self = traits::try_decode::<models::Entities, _, _>(buf)?;
125        entities
126            .try_validate()
127            .map_err(|e| ProtobufConversionError::InvalidValue(format!("invalid: {e}")).into())
128    }
129    fn decode_unchecked(buf: impl prost::bytes::Buf) -> Result<Self, traits::DecodeError> {
130        let msg = models::Entities::decode(buf)?;
131        // Skip TC computation for trusted data
132        let core_entities = entities_model_to_api(
133            msg,
134            cedar_policy_core::entities::TCComputation::AssumeAlreadyComputed,
135        )?;
136        Ok(Self(core_entities))
137    }
138}
139
140impl traits::Protobuf for api::PolicySet {
141    fn encode(&self) -> Result<Vec<u8>, traits::EncodeError> {
142        traits::encode_to_vec::<models::PolicySet, _>(self)
143    }
144    fn decode_unchecked(buf: impl prost::bytes::Buf) -> Result<Self, traits::DecodeError> {
145        traits::try_decode::<models::PolicySet, _, Self>(buf)
146    }
147}
148
149#[cfg(test)]
150mod roundtrip_test {
151    use super::models;
152    use prost::Message as _;
153    use std::{collections::HashMap, str::FromStr};
154
155    /// Performs a series of conversions: API -> Protobuf model -> Protobuf bytes -> Protobuf model -> API.
156    /// Checks that the input API policy set is equal to the converted policy set.
157    fn roundtrip_policies(policies: crate::PolicySet) {
158        // API -> Protobuf model
159        let policies_proto = models::PolicySet::from(&policies);
160        // Protobuf model -> Protobuf bytes
161        let buf = policies_proto.encode_to_vec();
162        // Protobuf bytes -> Protobuf model
163        let roundtripped_proto = models::PolicySet::decode(&buf[..])
164            .expect("Failed to deserialize PolicySet from protobuf");
165        // -> Protobuf model -> API
166        let roundtripped = crate::PolicySet::try_from(roundtripped_proto)
167            .expect("Failed to convert from protobuf to PolicySet");
168        similar_asserts::assert_eq!(policies, roundtripped);
169    }
170
171    fn roundtrip_policies_text(text: &str) {
172        let pset = crate::PolicySet::from_str(text).expect("Failed to parse policy set");
173        roundtrip_policies(pset);
174    }
175
176    #[test]
177    fn roundtrip_policyset_with_template_link() {
178        let mut pset = crate::PolicySet::from_str(
179            r#"
180            permit(principal == ?principal, action, resource);
181            "#,
182        )
183        .expect("Failed to parse policy set");
184        pset.link(
185            crate::PolicyId::new("policy0"),
186            crate::PolicyId::new("link0"),
187            HashMap::from([(
188                crate::SlotId::principal(),
189                crate::EntityUid::from_strs("User", "alice"),
190            )]),
191        )
192        .expect("Failed to link template");
193        roundtrip_policies(pset);
194    }
195
196    #[test]
197    fn roundtrip_policyset_empty() {
198        roundtrip_policies_text("");
199    }
200
201    #[test]
202    fn roundtrip_policyset_with_static_policy() {
203        roundtrip_policies_text(
204            r#"
205            permit(principal, action, resource);
206            "#,
207        );
208    }
209
210    #[test]
211    fn roundtrip_policyset_with_multiple_static_policies() {
212        roundtrip_policies_text(
213            r#"
214            permit(principal, action, resource);
215
216            forbid(principal, action, resource) when { context.is_restricted };
217
218            permit(principal == User::"alice", action == Action::"read", resource in Folder::"shared");
219            "#,
220        );
221    }
222
223    #[test]
224    fn roundtrip_policyset_with_when_and_unless() {
225        roundtrip_policies_text(
226            r#"
227            permit(principal, action, resource)
228                when { resource.owner == principal }
229                unless { principal.suspended };
230            "#,
231        );
232    }
233
234    #[test]
235    fn roundtrip_policyset_with_annotations() {
236        roundtrip_policies_text(
237            r#"
238            @advice("allow owner access")
239            permit(principal, action == Action::"write", resource)
240            when { resource.owner == principal };
241            "#,
242        );
243    }
244
245    #[test]
246    fn roundtrip_policyset_with_multiple_template_links() {
247        let mut pset = crate::PolicySet::from_str(
248            r#"
249            permit(principal == ?principal, action, resource in ?resource);
250            "#,
251        )
252        .expect("Failed to parse policy set");
253        pset.link(
254            crate::PolicyId::new("policy0"),
255            crate::PolicyId::new("link0"),
256            HashMap::from([
257                (
258                    crate::SlotId::principal(),
259                    crate::EntityUid::from_strs("User", "alice"),
260                ),
261                (
262                    crate::SlotId::resource(),
263                    crate::EntityUid::from_strs("Folder", "shared"),
264                ),
265            ]),
266        )
267        .expect("Failed to link template");
268        pset.link(
269            crate::PolicyId::new("policy0"),
270            crate::PolicyId::new("link1"),
271            HashMap::from([
272                (
273                    crate::SlotId::principal(),
274                    crate::EntityUid::from_strs("User", "bob"),
275                ),
276                (
277                    crate::SlotId::resource(),
278                    crate::EntityUid::from_strs("Folder", "private"),
279                ),
280            ]),
281        )
282        .expect("Failed to link template");
283        roundtrip_policies(pset);
284    }
285
286    #[test]
287    fn roundtrip_policyset_with_static_and_templates() {
288        let mut pset = crate::PolicySet::from_str(
289            r#"
290            forbid(principal, action, resource) unless { context.authenticated };
291
292            permit(principal == ?principal, action, resource);
293            "#,
294        )
295        .expect("Failed to parse policy set");
296        println!("{:?}", pset);
297        pset.link(
298            crate::PolicyId::new("policy1"),
299            crate::PolicyId::new("link0"),
300            HashMap::from([(
301                crate::SlotId::principal(),
302                crate::EntityUid::from_strs("User", "admin"),
303            )]),
304        )
305        .expect("Failed to link template");
306        roundtrip_policies(pset);
307    }
308
309    #[test]
310    fn roundtrip_policyset_with_is_constraint() {
311        roundtrip_policies_text(
312            r#"
313            permit(principal is User, action, resource is Folder);
314            "#,
315        );
316    }
317
318    #[test]
319    fn roundtrip_policyset_with_is_in_constraint() {
320        roundtrip_policies_text(
321            r#"
322            permit(principal is User in Group::"admins", action, resource);
323            "#,
324        );
325    }
326
327    #[test]
328    fn roundtrip_policyset_with_action_in_set() {
329        roundtrip_policies_text(
330            r#"
331            permit(principal, action in [Action::"read", Action::"list"], resource);
332            "#,
333        );
334    }
335
336    #[test]
337    fn roundtrip_policyset_with_extension_functions() {
338        roundtrip_policies_text(
339            r#"
340            forbid(principal, action, resource)
341                when { !context.src_ip.isInRange(ip("10.0.0.0/8")) };
342            "#,
343        );
344    }
345
346    #[test]
347    fn roundtrip_policyset_with_unlinked_template() {
348        roundtrip_policies_text(
349            r#"
350            permit(principal == ?principal, action, resource);
351            "#,
352        );
353    }
354}
355
356#[cfg(test)]
357mod decode_test {
358    use crate::proto::traits::Protobuf;
359    use cool_asserts::assert_matches;
360    use std::collections::HashMap;
361    use std::str::FromStr;
362
363    /// [`decode`] and [`decode_unchecked`] should produce the same data when they don't fail.
364    fn decode_eq_decode_unchecked<T: Protobuf + PartialEq>(x: T) {
365        let buf = x.encode().expect("encode failed");
366        let checked = T::decode(&buf[..]).expect("decode failed");
367        let unchecked = T::decode_unchecked(&buf[..]).expect("decode_unchecked failed");
368        similar_asserts::assert_eq!(checked, unchecked);
369    }
370
371    /// Decoding arbitrary bytes must never panic — it should return `Err`.
372    #[test]
373    fn decode_random_bytes_does_not_panic() {
374        use crate::proto::traits::Protobuf;
375
376        let inputs: &[&[u8]] = &[
377            b"",
378            b"\x00",
379            b"\xff\xff\xff\xff",
380            b"not a protobuf",
381            &[0u8; 1024],
382            &{
383                let mut v = Vec::new();
384                for i in 0u8..=255 {
385                    v.push(i);
386                }
387                v
388            },
389        ];
390
391        for input in inputs {
392            let _ = crate::Entity::decode(*input);
393            let _ = crate::Entities::decode(*input);
394            let _ = crate::Schema::decode(*input);
395            let _ = crate::EntityTypeName::decode(*input);
396            let _ = crate::EntityNamespace::decode(*input);
397            let _ = crate::Template::decode(*input);
398            let _ = crate::Expression::decode(*input);
399            let _ = crate::Request::decode(*input);
400            let _ = crate::PolicySet::decode(*input);
401        }
402    }
403
404    #[test]
405    fn decode_conversion_error_path() {
406        use crate::proto::traits::Protobuf;
407        // An Entity with a uid whose type name is empty string triggers
408        // ProtobufConversionError, exercising the DecodeError::Conversion path.
409        let model = crate::proto::models::Entity {
410            uid: Some(crate::proto::models::EntityUid {
411                ty: Some(crate::proto::models::Name {
412                    id: String::new(), // invalid: empty identifier
413                    path: vec![],
414                }),
415                eid: "x".to_string(),
416            }),
417            attrs: HashMap::new(),
418            ancestors: vec![],
419            tags: HashMap::new(),
420        };
421        let buf = prost::Message::encode_to_vec(&model);
422        assert_matches!(
423            crate::Entity::decode(&buf[..]),
424            Err(crate::proto::traits::DecodeError::Conversion(_))
425        );
426    }
427
428    #[test]
429    fn roundtrip_decode_unchecked_entities() {
430        let entities = crate::Entities::from_json_str(
431            r#"[
432                {"uid": {"type": "User", "id": "alice"}, "attrs": {"age": 25}, "parents": [{"type": "Group", "id": "admins"}]},
433                {"uid": {"type": "Group", "id": "admins"}, "attrs": {}, "parents": []}
434            ]"#,
435            None,
436        )
437        .expect("Failed to parse entities");
438        decode_eq_decode_unchecked::<crate::Entities>(entities);
439    }
440
441    #[test]
442    fn roundtrip_decode_unchecked_policy_set() {
443        let pset = crate::PolicySet::from_str(
444            r#"
445            permit(principal == User::"alice", action, resource);
446            forbid(principal, action, resource) when { context.restricted };
447            "#,
448        )
449        .expect("Failed to parse policy set");
450        decode_eq_decode_unchecked::<crate::PolicySet>(pset);
451    }
452
453    #[test]
454    fn roundtrip_decode_unchecked_template() {
455        let template =
456            crate::Template::from_str(r#"permit(principal == ?principal, action, resource);"#)
457                .expect("Failed to parse template");
458
459        decode_eq_decode_unchecked::<crate::Template>(template);
460    }
461
462    #[test]
463    fn roundtrip_decode_unchecked_entity() {
464        let entity = crate::Entity::from_json_value(
465            serde_json::json!({"uid": {"type": "User", "id": "bob"}, "attrs": {"active": true}, "parents": []}),
466            None,
467        )
468        .expect("Failed to parse entity");
469        decode_eq_decode_unchecked::<crate::Entity>(entity);
470    }
471
472    #[test]
473    fn roundtrip_decode_unchecked_request() {
474        let request = crate::Request::new(
475            crate::EntityUid::from_strs("User", "alice"),
476            crate::EntityUid::from_strs("Action", "read"),
477            crate::EntityUid::from_strs("Document", "doc1"),
478            crate::Context::empty(),
479            None,
480        )
481        .expect("Failed to create request");
482        decode_eq_decode_unchecked::<crate::Request>(request);
483    }
484}
485
486#[cfg(test)]
487mod encode_test {
488    use super::models;
489    use super::traits::{EncodeCheck, EncodeError, Protobuf, MAX_ENCODE_DEPTH};
490    use crate::proto::test_utils::*;
491    use crate::Expression;
492    use cedar_policy_core::ast;
493    use cool_asserts::assert_matches;
494    use std::str::FromStr;
495
496    // ================================================================
497    // Helpers for building deeply nested expressions of various kinds
498    // ================================================================
499
500    /// Build `n` levels of `!(!(...lit_bool(true)...))`.
501    fn deep_unary(n: usize) -> models::Expr {
502        let mut e = lit_bool(true);
503        for _ in 0..n {
504            e = not(e);
505        }
506        e
507    }
508
509    /// Build `n` levels of `{k: {k: ...lit_bool(true)...}}`.
510    fn deep_record_expr(n: usize) -> models::Expr {
511        let mut e = lit_bool(true);
512        for _ in 0..n {
513            e = record([("k", e)]);
514        }
515        e
516    }
517
518    /// Build `n` levels of `if true then (if true then ... else false) else false`.
519    fn deep_if(n: usize) -> models::Expr {
520        let mut e = lit_bool(true);
521        for _ in 0..n {
522            e = if_then_else(lit_bool(true), e, lit_bool(false));
523        }
524        e
525    }
526
527    /// Build `n` levels of `true && (true && ...)`.
528    fn deep_and(n: usize) -> models::Expr {
529        let mut e = lit_bool(true);
530        for _ in 0..n {
531            e = models::Expr {
532                expr_kind: Some(models::expr::ExprKind::And(Box::new(models::expr::And {
533                    left: Some(Box::new(lit_bool(true))),
534                    right: Some(Box::new(e)),
535                }))),
536            };
537        }
538        e
539    }
540
541    /// Build `n` levels of `true || (true || ...)`.
542    fn deep_or(n: usize) -> models::Expr {
543        let mut e = lit_bool(false);
544        for _ in 0..n {
545            e = models::Expr {
546                expr_kind: Some(models::expr::ExprKind::Or(Box::new(models::expr::Or {
547                    left: Some(Box::new(lit_bool(true))),
548                    right: Some(Box::new(e)),
549                }))),
550            };
551        }
552        e
553    }
554
555    /// Build `n` levels of `(... + 1) + 1`.
556    fn deep_binary(n: usize) -> models::Expr {
557        let mut e = lit_long(0);
558        for _ in 0..n {
559            e = binary(models::expr::binary_app::Op::Add, e, lit_long(1));
560        }
561        e
562    }
563
564    /// Build `n` levels of `ext(ext(...))`.
565    fn deep_ext(n: usize) -> models::Expr {
566        let mut e = lit_str("1.0");
567        for _ in 0..n {
568            e = ext_call("decimal", [e]);
569        }
570        e
571    }
572
573    /// Build `n` levels of `.attr.attr...`.
574    fn deep_get_attr(n: usize) -> models::Expr {
575        let mut e = var(models::expr::Var::Context);
576        for _ in 0..n {
577            e = get_attr(e, "x");
578        }
579        e
580    }
581
582    /// Build `n` levels of `has attr has attr ...`.
583    fn deep_has_attr(n: usize) -> models::Expr {
584        // has returns bool, so wrap: if (e has "x") then (inner has "x") else false
585        // Simpler: just chain `has` on nested get_attr
586        let mut e = var(models::expr::Var::Context);
587        for _ in 0..n {
588            e = has_attr(e, "x");
589        }
590        e
591    }
592
593    /// Build `n` levels of `like` wrapping.
594    fn deep_like(n: usize) -> models::Expr {
595        use models::expr::{like, ExprKind, Like};
596        // `like` takes an expr child, so we can nest: like(like(...))
597        // But `like` returns bool... use if to re-wrap.
598        // Simpler: just nest the expr child of Like.
599        let mut e = lit_str("hello");
600        for _ in 0..n {
601            e = models::Expr {
602                expr_kind: Some(ExprKind::Like(Box::new(Like {
603                    expr: Some(Box::new(e)),
604                    pattern: vec![
605                        like::PatternElem {
606                            data: Some(like::pattern_elem::Data::Wildcard(
607                                like::pattern_elem::Wildcard::Unit.into(),
608                            )),
609                        },
610                        like::PatternElem {
611                            data: Some(like::pattern_elem::Data::C("x".to_string())),
612                        },
613                    ],
614                }))),
615            };
616        }
617        e
618    }
619
620    /// Build `n` levels of `is` wrapping.
621    fn deep_is(n: usize) -> models::Expr {
622        let mut e = var(models::expr::Var::Principal);
623        for _ in 0..n {
624            e = models::Expr {
625                expr_kind: Some(models::expr::ExprKind::Is(Box::new(models::expr::Is {
626                    expr: Some(Box::new(e)),
627                    entity_type: Some(name("User")),
628                }))),
629            };
630        }
631        e
632    }
633
634    /// Build `n` levels of nested sets: `[[[[...]]]]`.
635    fn deep_set(n: usize) -> models::Expr {
636        let mut e = lit_long(1);
637        for _ in 0..n {
638            e = set([e]);
639        }
640        e
641    }
642
643    /// Maximum nesting levels that fit within `MAX_ENCODE_DEPTH`.
644    /// Each nesting costs 2 prost levels; root `Expr` costs 1.
645    /// So max nestings = (`MAX_ENCODE_DEPTH` - 1) / 2.
646    const MAX_NESTING: usize = (MAX_ENCODE_DEPTH - 1) / 2;
647
648    // ================================================================
649    // Depth limit tests for all expression variants
650    // ================================================================
651
652    /// Assert that the given expression passes or fails the encode check.
653    /// When `expect_ok`, also verifies the encode→decode roundtrip succeeds
654    /// (i.e., prost can decode what we encoded).
655    /// When `expect_still_decodes` and `expect_ok` is false, this checks that
656    /// `check_for_encode` returns an [`EncodeError::MaxDepthExceeded`] but
657    /// the encodeing then decoding still succeeds.
658    #[track_caller]
659    fn assert_encode_check(
660        name: &str,
661        expr: &models::Expr,
662        expect_ok: bool,
663        expect_still_decodes: bool,
664    ) {
665        if expect_ok {
666            assert!(
667                expr.check_for_encode().is_ok(),
668                "{name}: expected Ok but got MaxDepthExceeded"
669            );
670            // Verify prost can actually decode the encoded bytes.
671            let bytes = prost::Message::encode_to_vec(expr);
672            assert!(
673                <models::Expr as prost::Message>::decode(&bytes[..]).is_ok(),
674                "{name}: decoding failed despite depth check",
675            );
676        } else {
677            assert_matches!(
678                expr.check_for_encode(),
679                Err(EncodeError::MaxDepthExceeded),
680                "{name} did not error with MaxDepthExceeded"
681            );
682            // We still expect this to encode-decode
683            if expect_still_decodes {
684                let bytes = prost::Message::encode_to_vec(expr);
685                assert!(
686                    <models::Expr as prost::Message>::decode(&bytes[..]).is_ok(),
687                    "{name}: decoding failed when MaxDepthExceeded but expected to still decode",
688                );
689            }
690        }
691    }
692
693    #[test]
694    fn depth_all_expr_variants_at_limit() {
695        let builders: &[(&str, fn(usize) -> models::Expr, usize)] = &[
696            ("unary", deep_unary, MAX_NESTING),
697            ("record", deep_record_expr, (MAX_ENCODE_DEPTH - 1) / 3),
698            ("if", deep_if, MAX_NESTING),
699            ("and", deep_and, MAX_NESTING),
700            ("or", deep_or, MAX_NESTING),
701            ("binary", deep_binary, MAX_NESTING),
702            ("ext", deep_ext, MAX_NESTING),
703            ("get_attr", deep_get_attr, MAX_NESTING),
704            ("has_attr", deep_has_attr, MAX_NESTING),
705            ("like", deep_like, MAX_NESTING),
706            ("is", deep_is, MAX_NESTING),
707            ("set", deep_set, MAX_NESTING),
708        ];
709        for (name, builder, limit) in builders {
710            let ok = builder(*limit);
711            assert_encode_check(name, &ok, true, true); // check succeeds, decodes
712            let bad = builder(*limit + 1);
713            assert_encode_check(name, &bad, false, true); // check fails, still decodes
714            let bad = builder(*limit + 2);
715            assert_encode_check(name, &bad, false, true); // check fails, still decodes
716            let bad = builder(*limit + 8);
717            assert_encode_check(name, &bad, false, false); // check fails, fails decode
718        }
719    }
720
721    #[test]
722    fn depth_lit_euid_at_limit() {
723        // A lit_euid leaf adds 3 extra levels: (Literal(EntityUid(Name(..))))
724        // The leaf Expr is at depth 1 + 2*n, so the Name is at 1 + 2*n + 3.
725        // Max n where 1 + 2*n + 3 <= MAX_ENCODE_DEPTH: n = (MAX_ENCODE_DEPTH - 4) / 2
726        let max_euid_nesting = MAX_ENCODE_DEPTH / 2 - 2;
727
728        let mut ok = lit_euid("User", "alice");
729        for _ in 0..max_euid_nesting {
730            ok = not(ok);
731        }
732        assert_encode_check("lit_euid_ok", &ok, true, true);
733
734        let mut bad = lit_euid("User", "alice");
735        for _ in 0..=max_euid_nesting {
736            bad = not(bad);
737        }
738        assert_encode_check("lit_euid_bad", &bad, false, true);
739    }
740
741    #[test]
742    fn depth_ext_fn_name_without_args() {
743        // An ExtApp with no args but fn_name present should still be caught.
744        let target_depth = MAX_ENCODE_DEPTH - 1;
745        let nesting = (target_depth - 1) / 2;
746        let ext_no_args = models::Expr {
747            expr_kind: Some(models::expr::ExprKind::ExtApp(
748                models::expr::ExtensionFunctionApp {
749                    fn_name: Some(name("decimal")),
750                    args: vec![],
751                },
752            )),
753        };
754        let mut e = ext_no_args;
755        for _ in 0..nesting {
756            e = not(e);
757        }
758        assert_encode_check("ext_fn_name_no_args", &e, false, true);
759    }
760
761    // ================================================================
762    // Full API encode path tests
763    // ================================================================
764
765    #[test]
766    fn encode_expression_api_returns_error_on_deep_expr() {
767        let mut e = ast::Expr::var(ast::Var::Principal);
768        for _ in 0..=MAX_NESTING {
769            e = ast::Expr::not(e);
770        }
771        let expression = crate::Expression(e);
772        assert_matches!(expression.encode(), Err(EncodeError::MaxDepthExceeded));
773    }
774
775    #[test]
776    fn encode_policyset_with_deep_condition_fails() {
777        let mut e = ast::Expr::val(true);
778        for _ in 0..=MAX_NESTING {
779            e = ast::Expr::not(e);
780        }
781        let template = ast::Template::new(
782            ast::PolicyID::from_string("deep_policy"),
783            None,
784            ast::Annotations::new(),
785            ast::Effect::Permit,
786            ast::PrincipalConstraint::any(),
787            ast::ActionConstraint::any(),
788            ast::ResourceConstraint::any(),
789            Some(e),
790        );
791        let mut pset = ast::PolicySet::new();
792        pset.add_template(template).expect("add template");
793        let api_pset = crate::PolicySet::from_ast(pset);
794        assert_matches!(api_pset.encode(), Err(EncodeError::MaxDepthExceeded));
795    }
796
797    #[test]
798    fn encode_entities_with_deep_attr_fails() {
799        let deep_expr = deep_unary(MAX_NESTING + 1);
800        let ent = entity("User", "alice", [("deep_attr", deep_expr)]);
801        assert_matches!(ent.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
802    }
803
804    #[test]
805    fn encode_entity_with_deep_tag_fails() {
806        let deep_expr = deep_unary(MAX_NESTING + 1);
807        let ent = entity_full("User", "alice", [], [], [("deep_tag", deep_expr)]);
808        assert_matches!(ent.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
809    }
810
811    #[test]
812    fn encode_request_with_deep_context_fails() {
813        let deep_expr = deep_unary(MAX_NESTING + 1);
814        let req = models::Request {
815            principal: Some(entity_uid(name("User"), "alice")),
816            action: Some(entity_uid(name("Action"), "read")),
817            resource: Some(entity_uid(qualified_name("Doc", &["MyApp"]), "readme")),
818            context: [("deep".to_string(), deep_expr)].into_iter().collect(),
819        };
820        assert_matches!(req.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
821    }
822
823    #[test]
824    fn encode_request_shallow_context_succeeds() {
825        let req = models::Request {
826            principal: Some(entity_uid(name("User"), "alice")),
827            action: Some(entity_uid(name("Action"), "read")),
828            resource: Some(entity_uid(qualified_name("Doc", &["MyApp"]), "readme")),
829            context: [("flag".to_string(), lit_bool(true))].into_iter().collect(),
830        };
831        assert!(req.check_for_encode().is_ok());
832    }
833
834    // ================================================================
835    // Schema type depth tests
836    // ================================================================
837
838    /// Build `n` levels of `Set(Set(...Long...))`.
839    fn deep_set_type(n: usize) -> models::Type {
840        let mut ty = long_type();
841        for _ in 0..n {
842            ty = set_type(ty);
843        }
844        ty
845    }
846
847    /// Build `n` levels of `Record { x: Record { x: ... Long } }`.
848    fn deep_record_type(n: usize) -> models::Type {
849        let mut ty = long_type();
850        for _ in 0..n {
851            ty = record_type([("x", required(ty))]);
852        }
853        ty
854    }
855
856    #[test]
857    fn encode_schema_set_type_limit() {
858        // Set nesting costs 1 prost level per level. The starting depth for
859        // `check_type_depth_inner` when called through Schema → entity attributes is:
860        //   check_for_encode starts at init=1, Schema adds +3, check_type_depth adds +2 = 6.
861        // Exceeds when 6 + n > MAX_ENCODE_DEPTH.
862        let max_set_depth = MAX_ENCODE_DEPTH - 6;
863        let ok_schema = schema([entity_decl(
864            "Foo",
865            [("a", required(deep_set_type(max_set_depth)))],
866        )]);
867        assert!(ok_schema.check_for_encode().is_ok());
868
869        let bad_schema = schema([entity_decl(
870            "Foo",
871            [("a", required(deep_set_type(max_set_depth + 1)))],
872        )]);
873        assert_matches!(
874            bad_schema.check_for_encode(),
875            Err(EncodeError::MaxDepthExceeded)
876        );
877        // Just over the limit, prost can still encode+decode (our limit is conservative)
878        let bytes = prost::Message::encode_to_vec(&bad_schema);
879        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
880    }
881
882    #[test]
883    fn encode_schema_record_type_limit() {
884        // Record nesting costs 4 prost levels per level (Record msg + map entry +
885        // AttributeType + Type). The starting depth for `check_type_depth_inner`
886        // through Schema → entity attributes is 6
887        // (init=1, Schema +3, check_type_depth +2).
888        // Exceeds when 6 + 4*n > MAX_ENCODE_DEPTH.
889        let max_rec_depth = (MAX_ENCODE_DEPTH - 6) / 4;
890        let ok_schema = schema([entity_decl(
891            "Bar",
892            [("a", required(deep_record_type(max_rec_depth)))],
893        )]);
894        assert!(ok_schema.check_for_encode().is_ok());
895
896        let bad_schema = schema([entity_decl(
897            "Bar",
898            [("a", required(deep_record_type(max_rec_depth + 1)))],
899        )]);
900        assert_matches!(
901            bad_schema.check_for_encode(),
902            Err(EncodeError::MaxDepthExceeded)
903        );
904        // Just over the limit, we can still decode
905        let bytes = prost::Message::encode_to_vec(&bad_schema);
906        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
907    }
908
909    #[test]
910    fn encode_schema_tag_set_type_limit() {
911        // Tags path: init=1, Schema impl passes init+3=4 to check_type_depth_inner.
912        // So Set nesting exceeds when 4 + n > MAX_ENCODE_DEPTH.
913        let max_tag_set = MAX_ENCODE_DEPTH - 4;
914        let ok_decl = entity_decl_full("Foo", [], [], Some(deep_set_type(max_tag_set)));
915        let ok = schema([ok_decl]);
916        assert!(ok.check_for_encode().is_ok());
917
918        let bad_decl = entity_decl_full("Foo", [], [], Some(deep_set_type(max_tag_set + 1)));
919        let bad = schema([bad_decl]);
920        assert_matches!(bad.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
921        // Just over the limit, we can still decode
922        let bytes = prost::Message::encode_to_vec(&bad);
923        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
924    }
925
926    #[test]
927    fn encode_schema_tag_record_type_limit() {
928        // Tags path starts at depth 4; record nesting costs 4 per level.
929        // Exceeds when 4 + 4*n > MAX_ENCODE_DEPTH.
930        let max_tag_rec = (MAX_ENCODE_DEPTH - 4) / 4;
931        let ok_decl = entity_decl_full("Foo", [], [], Some(deep_record_type(max_tag_rec)));
932        let ok = schema([ok_decl]);
933        assert!(ok.check_for_encode().is_ok());
934
935        let bad_decl = entity_decl_full("Foo", [], [], Some(deep_record_type(max_tag_rec + 1)));
936        let bad = schema([bad_decl]);
937        assert_matches!(bad.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
938        // Just over the limit, we can still decode
939        let bytes = prost::Message::encode_to_vec(&bad);
940        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
941    }
942
943    #[test]
944    fn encode_schema_action_context_set_type_limit() {
945        // Action context path: init=1, Schema impl passes init+3=4 to check_type_depth,
946        // which adds +2 = 6 before entering check_type_depth_inner (same as entity attrs).
947        // Exceeds when 6 + n > MAX_ENCODE_DEPTH.
948        let max_ctx_set = MAX_ENCODE_DEPTH - 6;
949        let ok_action = action_decl(
950            ("Action", "read"),
951            ["User"],
952            ["Doc"],
953            [("ctx", required(deep_set_type(max_ctx_set)))],
954        );
955        let ok = schema_full(
956            [entity_decl("User", []), entity_decl("Doc", [])],
957            [ok_action],
958        );
959        assert!(ok.check_for_encode().is_ok());
960
961        let bad_action = action_decl(
962            ("Action", "read"),
963            ["User"],
964            ["Doc"],
965            [("ctx", required(deep_set_type(max_ctx_set + 1)))],
966        );
967        let bad = schema_full(
968            [entity_decl("User", []), entity_decl("Doc", [])],
969            [bad_action],
970        );
971        assert_matches!(bad.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
972        // Just over the limit, we can still decode
973        let bytes = prost::Message::encode_to_vec(&bad);
974        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
975    }
976
977    #[test]
978    fn encode_schema_action_context_record_type_limit() {
979        // Action context starts at depth 6; record costs 4 per level.
980        let max_ctx_rec = (MAX_ENCODE_DEPTH - 6) / 4;
981        let ok_action = action_decl(
982            ("Action", "write"),
983            ["User"],
984            ["Doc"],
985            [("ctx", required(deep_record_type(max_ctx_rec)))],
986        );
987        let ok = schema_full(
988            [entity_decl("User", []), entity_decl("Doc", [])],
989            [ok_action],
990        );
991        assert!(ok.check_for_encode().is_ok());
992
993        let bad_action = action_decl(
994            ("Action", "write"),
995            ["User"],
996            ["Doc"],
997            [("ctx", required(deep_record_type(max_ctx_rec + 1)))],
998        );
999        let bad = schema_full(
1000            [entity_decl("User", []), entity_decl("Doc", [])],
1001            [bad_action],
1002        );
1003        assert_matches!(bad.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
1004        // Just over the limit, we can still decode
1005        let bytes = prost::Message::encode_to_vec(&bad);
1006        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
1007    }
1008
1009    #[test]
1010    fn encode_schema_entity_type_leaf_at_limit() {
1011        // Entity/Ext leaf types contain a Name message at depth + 1.
1012        // Through Schema → entity attributes: starting depth is 6.
1013        // Set nesting of n gives the leaf Type at depth 6 + n.
1014        // The Name inside Entity/Ext is at 6 + n + 1.
1015        // Max n where 6 + n + 1 <= MAX_ENCODE_DEPTH: n = MAX_ENCODE_DEPTH - 7
1016        let max_set_depth = MAX_ENCODE_DEPTH - 7;
1017
1018        // Build Set(Set(...Entity...)) with entity_type leaf
1019        let mut ok_ty = entity_type("User");
1020        for _ in 0..max_set_depth {
1021            ok_ty = set_type(ok_ty);
1022        }
1023        let ok_schema = schema([entity_decl("Foo", [("a", required(ok_ty))])]);
1024        assert!(ok_schema.check_for_encode().is_ok());
1025
1026        let mut bad_ty = entity_type("User");
1027        for _ in 0..=max_set_depth {
1028            bad_ty = set_type(bad_ty);
1029        }
1030        let bad_schema = schema([entity_decl("Foo", [("a", required(bad_ty))])]);
1031        assert_matches!(
1032            bad_schema.check_for_encode(),
1033            Err(EncodeError::MaxDepthExceeded)
1034        );
1035        // Just over the limit, prost can still decode
1036        let bytes = prost::Message::encode_to_vec(&bad_schema);
1037        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
1038
1039        // Same test with extension_type leaf
1040        let mut ok_ext = extension_type("decimal");
1041        for _ in 0..max_set_depth {
1042            ok_ext = set_type(ok_ext);
1043        }
1044        let ok_schema = schema([entity_decl("Bar", [("b", required(ok_ext))])]);
1045        assert!(ok_schema.check_for_encode().is_ok());
1046
1047        let mut bad_ext = extension_type("decimal");
1048        for _ in 0..=max_set_depth {
1049            bad_ext = set_type(bad_ext);
1050        }
1051        let bad_schema = schema([entity_decl("Bar", [("b", required(bad_ext))])]);
1052        assert_matches!(
1053            bad_schema.check_for_encode(),
1054            Err(EncodeError::MaxDepthExceeded)
1055        );
1056        // Just over the limit, prost can still decode
1057        let bytes = prost::Message::encode_to_vec(&bad_schema);
1058        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
1059    }
1060
1061    #[test]
1062    fn encode_schema_multiple_entities_one_deep_fails() {
1063        // Only one entity has a deeply nested type; the check should still catch it.
1064        let max_set_depth = MAX_ENCODE_DEPTH - 6;
1065        let s = schema([
1066            entity_decl("Shallow", [("x", required(long_type()))]),
1067            entity_decl("Deep", [("a", required(deep_set_type(max_set_depth + 1)))]),
1068        ]);
1069        assert_matches!(s.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
1070        // Just over the limit, we can still decode
1071        let bytes = prost::Message::encode_to_vec(&s);
1072        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
1073    }
1074
1075    #[test]
1076    fn encode_schema_multiple_actions_one_deep_fails() {
1077        let max_ctx_set = MAX_ENCODE_DEPTH - 6;
1078        let shallow_action = action_decl(
1079            ("Action", "read"),
1080            ["User"],
1081            ["Doc"],
1082            [("flag", required(bool_type()))],
1083        );
1084        let deep_action = action_decl(
1085            ("Action", "write"),
1086            ["User"],
1087            ["Doc"],
1088            [("ctx", required(deep_set_type(max_ctx_set + 1)))],
1089        );
1090        let s = schema_full(
1091            [entity_decl("User", []), entity_decl("Doc", [])],
1092            [shallow_action, deep_action],
1093        );
1094        assert_matches!(s.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
1095        // Just over the limit, we can still decode
1096        let bytes = prost::Message::encode_to_vec(&s);
1097        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
1098    }
1099
1100    #[test]
1101    fn encode_schema_shallow_type_succeeds() {
1102        let (schema, _) =
1103            crate::Schema::from_cedarschema_str("entity User { name: String, age: Long };")
1104                .expect("parse schema");
1105        assert!(schema.encode().is_ok());
1106    }
1107
1108    #[test]
1109    fn encode_schema_mixed_nesting_succeeds() {
1110        let nested_type = record_type([
1111            ("inner_set", required(set_type(set_type(long_type())))),
1112            (
1113                "inner_rec",
1114                optional(record_type([("x", required(string_type()))])),
1115            ),
1116        ]);
1117        let s = schema([entity_decl("Complex", [("data", required(nested_type))])]);
1118        assert!(s.check_for_encode().is_ok());
1119    }
1120
1121    // ================================================================
1122    // Roundtrip test (encode at limit → decode succeeds)
1123    // ================================================================
1124
1125    #[test]
1126    fn encode_shallow_expression_succeeds() {
1127        let expression = crate::Expression::from_str("1 + 2").expect("parse");
1128        assert!(expression.encode().is_ok());
1129    }
1130
1131    #[test]
1132    fn encode_at_limit_roundtrips_through_prost() {
1133        let expr = deep_unary(MAX_NESTING);
1134        assert!(expr.check_for_encode().is_ok());
1135        // Encode via the API and decode to confirm prost doesn't reject it.
1136        let mut e = ast::Expr::val(true);
1137        for _ in 0..MAX_NESTING {
1138            e = ast::Expr::not(e);
1139        }
1140        let expression = crate::Expression(e);
1141        let buf = expression.encode().expect("should encode within limit");
1142        Expression::decode(&buf[..]).expect("should decode within prost's recursion limit");
1143    }
1144}