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_policyset_link_depth_check() {
799        // A PolicySet with links should pass the depth check since links are
800        // structurally shallow (Policy → EntityUid → Name = 3 levels).
801        let policy_set = models::PolicySet {
802            templates: vec![],
803            links: vec![models::Policy {
804                template_id: "t1".to_string(),
805                link_id: Some("link1".to_string()),
806                is_template_link: true,
807                principal_euid: Some(entity_uid(name("User"), "alice")),
808                resource_euid: Some(entity_uid(qualified_name("Doc", &["MyApp"]), "readme")),
809            }],
810        };
811        assert!(policy_set.check_for_encode().is_ok());
812
813        // Checking at max depth - 1 results in error
814        assert_matches!(
815            policy_set.check_for_encode_from_depth(MAX_ENCODE_DEPTH - 1),
816            Err(EncodeError::MaxDepthExceeded)
817        );
818    }
819
820    #[test]
821    fn encode_entities_with_deep_attr_fails() {
822        let deep_expr = deep_unary(MAX_NESTING + 1);
823        let ent = entity("User", "alice", [("deep_attr", deep_expr)]);
824        assert_matches!(ent.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
825    }
826
827    #[test]
828    fn encode_entity_with_deep_tag_fails() {
829        let deep_expr = deep_unary(MAX_NESTING + 1);
830        let ent = entity_full("User", "alice", [], [], [("deep_tag", deep_expr)]);
831        assert_matches!(ent.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
832    }
833
834    #[test]
835    fn encode_request_with_deep_context_fails() {
836        let deep_expr = deep_unary(MAX_NESTING + 1);
837        let req = models::Request {
838            principal: Some(entity_uid(name("User"), "alice")),
839            action: Some(entity_uid(name("Action"), "read")),
840            resource: Some(entity_uid(qualified_name("Doc", &["MyApp"]), "readme")),
841            context: [("deep".to_string(), deep_expr)].into_iter().collect(),
842        };
843        assert_matches!(req.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
844    }
845
846    #[test]
847    fn encode_request_shallow_context_succeeds() {
848        let req = models::Request {
849            principal: Some(entity_uid(name("User"), "alice")),
850            action: Some(entity_uid(name("Action"), "read")),
851            resource: Some(entity_uid(qualified_name("Doc", &["MyApp"]), "readme")),
852            context: [("flag".to_string(), lit_bool(true))].into_iter().collect(),
853        };
854        assert!(req.check_for_encode().is_ok());
855    }
856
857    // ================================================================
858    // Schema type depth tests
859    // ================================================================
860
861    /// Build `n` levels of `Set(Set(...Long...))`.
862    fn deep_set_type(n: usize) -> models::Type {
863        let mut ty = long_type();
864        for _ in 0..n {
865            ty = set_type(ty);
866        }
867        ty
868    }
869
870    /// Build `n` levels of `Record { x: Record { x: ... Long } }`.
871    fn deep_record_type(n: usize) -> models::Type {
872        let mut ty = long_type();
873        for _ in 0..n {
874            ty = record_type([("x", required(ty))]);
875        }
876        ty
877    }
878
879    #[test]
880    fn encode_schema_set_type_limit() {
881        // Set nesting costs 1 prost level per level. The starting depth for
882        // `check_type_depth_inner` when called through Schema → entity attributes is:
883        //   check_for_encode starts at init=1, Schema adds +3, check_type_depth adds +2 = 6.
884        // Exceeds when 6 + n > MAX_ENCODE_DEPTH.
885        let max_set_depth = MAX_ENCODE_DEPTH - 6;
886        let ok_schema = schema([entity_decl(
887            "Foo",
888            [("a", required(deep_set_type(max_set_depth)))],
889        )]);
890        assert!(ok_schema.check_for_encode().is_ok());
891
892        let bad_schema = schema([entity_decl(
893            "Foo",
894            [("a", required(deep_set_type(max_set_depth + 1)))],
895        )]);
896        assert_matches!(
897            bad_schema.check_for_encode(),
898            Err(EncodeError::MaxDepthExceeded)
899        );
900        // Just over the limit, prost can still encode+decode (our limit is conservative)
901        let bytes = prost::Message::encode_to_vec(&bad_schema);
902        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
903    }
904
905    #[test]
906    fn encode_schema_record_type_limit() {
907        // Record nesting costs 4 prost levels per level (Record msg + map entry +
908        // AttributeType + Type). The starting depth for `check_type_depth_inner`
909        // through Schema → entity attributes is 6
910        // (init=1, Schema +3, check_type_depth +2).
911        // Exceeds when 6 + 4*n > MAX_ENCODE_DEPTH.
912        let max_rec_depth = (MAX_ENCODE_DEPTH - 6) / 4;
913        let ok_schema = schema([entity_decl(
914            "Bar",
915            [("a", required(deep_record_type(max_rec_depth)))],
916        )]);
917        assert!(ok_schema.check_for_encode().is_ok());
918
919        let bad_schema = schema([entity_decl(
920            "Bar",
921            [("a", required(deep_record_type(max_rec_depth + 1)))],
922        )]);
923        assert_matches!(
924            bad_schema.check_for_encode(),
925            Err(EncodeError::MaxDepthExceeded)
926        );
927        // Just over the limit, we can still decode
928        let bytes = prost::Message::encode_to_vec(&bad_schema);
929        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
930    }
931
932    #[test]
933    fn encode_schema_tag_set_type_limit() {
934        // Tags path: init=1, Schema impl passes init+3=4 to check_type_depth_inner.
935        // So Set nesting exceeds when 4 + n > MAX_ENCODE_DEPTH.
936        let max_tag_set = MAX_ENCODE_DEPTH - 4;
937        let ok_decl = entity_decl_full("Foo", [], [], Some(deep_set_type(max_tag_set)));
938        let ok = schema([ok_decl]);
939        assert!(ok.check_for_encode().is_ok());
940
941        let bad_decl = entity_decl_full("Foo", [], [], Some(deep_set_type(max_tag_set + 1)));
942        let bad = schema([bad_decl]);
943        assert_matches!(bad.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
944        // Just over the limit, we can still decode
945        let bytes = prost::Message::encode_to_vec(&bad);
946        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
947    }
948
949    #[test]
950    fn encode_schema_tag_record_type_limit() {
951        // Tags path starts at depth 4; record nesting costs 4 per level.
952        // Exceeds when 4 + 4*n > MAX_ENCODE_DEPTH.
953        let max_tag_rec = (MAX_ENCODE_DEPTH - 4) / 4;
954        let ok_decl = entity_decl_full("Foo", [], [], Some(deep_record_type(max_tag_rec)));
955        let ok = schema([ok_decl]);
956        assert!(ok.check_for_encode().is_ok());
957
958        let bad_decl = entity_decl_full("Foo", [], [], Some(deep_record_type(max_tag_rec + 1)));
959        let bad = schema([bad_decl]);
960        assert_matches!(bad.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
961        // Just over the limit, we can still decode
962        let bytes = prost::Message::encode_to_vec(&bad);
963        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
964    }
965
966    #[test]
967    fn encode_schema_action_context_set_type_limit() {
968        // Action context path: init=1, Schema impl passes init+3=4 to check_type_depth,
969        // which adds +2 = 6 before entering check_type_depth_inner (same as entity attrs).
970        // Exceeds when 6 + n > MAX_ENCODE_DEPTH.
971        let max_ctx_set = MAX_ENCODE_DEPTH - 6;
972        let ok_action = action_decl(
973            ("Action", "read"),
974            ["User"],
975            ["Doc"],
976            [("ctx", required(deep_set_type(max_ctx_set)))],
977        );
978        let ok = schema_full(
979            [entity_decl("User", []), entity_decl("Doc", [])],
980            [ok_action],
981        );
982        assert!(ok.check_for_encode().is_ok());
983
984        let bad_action = action_decl(
985            ("Action", "read"),
986            ["User"],
987            ["Doc"],
988            [("ctx", required(deep_set_type(max_ctx_set + 1)))],
989        );
990        let bad = schema_full(
991            [entity_decl("User", []), entity_decl("Doc", [])],
992            [bad_action],
993        );
994        assert_matches!(bad.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
995        // Just over the limit, we can still decode
996        let bytes = prost::Message::encode_to_vec(&bad);
997        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
998    }
999
1000    #[test]
1001    fn encode_schema_action_context_record_type_limit() {
1002        // Action context starts at depth 6; record costs 4 per level.
1003        let max_ctx_rec = (MAX_ENCODE_DEPTH - 6) / 4;
1004        let ok_action = action_decl(
1005            ("Action", "write"),
1006            ["User"],
1007            ["Doc"],
1008            [("ctx", required(deep_record_type(max_ctx_rec)))],
1009        );
1010        let ok = schema_full(
1011            [entity_decl("User", []), entity_decl("Doc", [])],
1012            [ok_action],
1013        );
1014        assert!(ok.check_for_encode().is_ok());
1015
1016        let bad_action = action_decl(
1017            ("Action", "write"),
1018            ["User"],
1019            ["Doc"],
1020            [("ctx", required(deep_record_type(max_ctx_rec + 1)))],
1021        );
1022        let bad = schema_full(
1023            [entity_decl("User", []), entity_decl("Doc", [])],
1024            [bad_action],
1025        );
1026        assert_matches!(bad.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
1027        // Just over the limit, we can still decode
1028        let bytes = prost::Message::encode_to_vec(&bad);
1029        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
1030    }
1031
1032    #[test]
1033    fn encode_schema_entity_type_leaf_at_limit() {
1034        // Entity/Ext leaf types contain a Name message at depth + 1.
1035        // Through Schema → entity attributes: starting depth is 6.
1036        // Set nesting of n gives the leaf Type at depth 6 + n.
1037        // The Name inside Entity/Ext is at 6 + n + 1.
1038        // Max n where 6 + n + 1 <= MAX_ENCODE_DEPTH: n = MAX_ENCODE_DEPTH - 7
1039        let max_set_depth = MAX_ENCODE_DEPTH - 7;
1040
1041        // Build Set(Set(...Entity...)) with entity_type leaf
1042        let mut ok_ty = entity_type("User");
1043        for _ in 0..max_set_depth {
1044            ok_ty = set_type(ok_ty);
1045        }
1046        let ok_schema = schema([entity_decl("Foo", [("a", required(ok_ty))])]);
1047        assert!(ok_schema.check_for_encode().is_ok());
1048
1049        let mut bad_ty = entity_type("User");
1050        for _ in 0..=max_set_depth {
1051            bad_ty = set_type(bad_ty);
1052        }
1053        let bad_schema = schema([entity_decl("Foo", [("a", required(bad_ty))])]);
1054        assert_matches!(
1055            bad_schema.check_for_encode(),
1056            Err(EncodeError::MaxDepthExceeded)
1057        );
1058        // Just over the limit, prost can still decode
1059        let bytes = prost::Message::encode_to_vec(&bad_schema);
1060        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
1061
1062        // Same test with extension_type leaf
1063        let mut ok_ext = extension_type("decimal");
1064        for _ in 0..max_set_depth {
1065            ok_ext = set_type(ok_ext);
1066        }
1067        let ok_schema = schema([entity_decl("Bar", [("b", required(ok_ext))])]);
1068        assert!(ok_schema.check_for_encode().is_ok());
1069
1070        let mut bad_ext = extension_type("decimal");
1071        for _ in 0..=max_set_depth {
1072            bad_ext = set_type(bad_ext);
1073        }
1074        let bad_schema = schema([entity_decl("Bar", [("b", required(bad_ext))])]);
1075        assert_matches!(
1076            bad_schema.check_for_encode(),
1077            Err(EncodeError::MaxDepthExceeded)
1078        );
1079        // Just over the limit, prost can still decode
1080        let bytes = prost::Message::encode_to_vec(&bad_schema);
1081        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
1082    }
1083
1084    #[test]
1085    fn encode_schema_multiple_entities_one_deep_fails() {
1086        // Only one entity has a deeply nested type; the check should still catch it.
1087        let max_set_depth = MAX_ENCODE_DEPTH - 6;
1088        let s = schema([
1089            entity_decl("Shallow", [("x", required(long_type()))]),
1090            entity_decl("Deep", [("a", required(deep_set_type(max_set_depth + 1)))]),
1091        ]);
1092        assert_matches!(s.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
1093        // Just over the limit, we can still decode
1094        let bytes = prost::Message::encode_to_vec(&s);
1095        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
1096    }
1097
1098    #[test]
1099    fn encode_schema_multiple_actions_one_deep_fails() {
1100        let max_ctx_set = MAX_ENCODE_DEPTH - 6;
1101        let shallow_action = action_decl(
1102            ("Action", "read"),
1103            ["User"],
1104            ["Doc"],
1105            [("flag", required(bool_type()))],
1106        );
1107        let deep_action = action_decl(
1108            ("Action", "write"),
1109            ["User"],
1110            ["Doc"],
1111            [("ctx", required(deep_set_type(max_ctx_set + 1)))],
1112        );
1113        let s = schema_full(
1114            [entity_decl("User", []), entity_decl("Doc", [])],
1115            [shallow_action, deep_action],
1116        );
1117        assert_matches!(s.check_for_encode(), Err(EncodeError::MaxDepthExceeded));
1118        // Just over the limit, we can still decode
1119        let bytes = prost::Message::encode_to_vec(&s);
1120        assert!(<models::Schema as prost::Message>::decode(&bytes[..]).is_ok());
1121    }
1122
1123    #[test]
1124    fn encode_schema_shallow_type_succeeds() {
1125        let (schema, _) =
1126            crate::Schema::from_cedarschema_str("entity User { name: String, age: Long };")
1127                .expect("parse schema");
1128        assert!(schema.encode().is_ok());
1129    }
1130
1131    #[test]
1132    fn encode_schema_mixed_nesting_succeeds() {
1133        let nested_type = record_type([
1134            ("inner_set", required(set_type(set_type(long_type())))),
1135            (
1136                "inner_rec",
1137                optional(record_type([("x", required(string_type()))])),
1138            ),
1139        ]);
1140        let s = schema([entity_decl("Complex", [("data", required(nested_type))])]);
1141        assert!(s.check_for_encode().is_ok());
1142    }
1143
1144    // ================================================================
1145    // Roundtrip test (encode at limit → decode succeeds)
1146    // ================================================================
1147
1148    #[test]
1149    fn encode_shallow_expression_succeeds() {
1150        let expression = crate::Expression::from_str("1 + 2").expect("parse");
1151        assert!(expression.encode().is_ok());
1152    }
1153
1154    #[test]
1155    fn encode_at_limit_roundtrips_through_prost() {
1156        let expr = deep_unary(MAX_NESTING);
1157        assert!(expr.check_for_encode().is_ok());
1158        // Encode via the API and decode to confirm prost doesn't reject it.
1159        let mut e = ast::Expr::val(true);
1160        for _ in 0..MAX_NESTING {
1161            e = ast::Expr::not(e);
1162        }
1163        let expression = crate::Expression(e);
1164        let buf = expression.encode().expect("should encode within limit");
1165        Expression::decode(&buf[..]).expect("should decode within prost's recursion limit");
1166    }
1167}