cedar_policy/proto/traits.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 cedar_policy_core::{
18 ast::{ExprValidationError, Infallible, PolicySetValidationError, TemplateValidationError},
19 entities::err::EntitiesError,
20 validator::SchemaError,
21};
22
23use crate::api;
24
25use super::ast::ProtobufConversionError;
26
27/// Error type for protobuf decoding failures
28#[derive(Debug, thiserror::Error)]
29pub enum DecodeError {
30 /// The input buffer does not contain a valid protobuf message
31 #[error(transparent)]
32 Proto(prost::DecodeError),
33 /// The protobuf message was well-formed but its contents could not be
34 /// converted into the target Cedar type
35 #[error(transparent)]
36 Conversion(ProtobufConversionError),
37}
38
39impl From<prost::DecodeError> for DecodeError {
40 fn from(e: prost::DecodeError) -> Self {
41 Self::Proto(e)
42 }
43}
44
45impl From<ProtobufConversionError> for DecodeError {
46 fn from(e: ProtobufConversionError) -> Self {
47 Self::Conversion(e)
48 }
49}
50
51/// Error type for protobuf encoding failures
52#[derive(Debug, thiserror::Error)]
53#[non_exhaustive]
54pub enum EncodeError {
55 /// The input data contains too many recursion levels to be encoded
56 #[error("data structure depth exceeds maximum encodable depth of {MAX_ENCODE_DEPTH}")]
57 MaxDepthExceeded,
58 /// The protobuf message could not be encoded (e.g., buffer too small)
59 #[error(transparent)]
60 Proto(#[from] prost::EncodeError),
61}
62
63/// Maximum allowed protobuf recursion depth for encoding.
64///
65/// Prost's decoder has a hardcoded recursion limit of 100, where each nested
66/// message entry counts as one level. We use this limit when calculating the
67/// depth of a `prost::Message` (i.e. the `models::...`), not the depth of
68/// syntax tree (e.g. type or expression).
69/// Typically, the depth of a model is twice the one of the internal representation's
70/// tree.
71///
72/// We set this to 90 to leave a small margin (10 levels) for outer wrappers
73/// like `TemplateBody` or `Entity` that contain an `Expr` field.
74pub const MAX_ENCODE_DEPTH: usize = 90;
75
76/// A trait for protobuf model types that require validation before encoding.
77///
78/// Model types implement this to perform structural checks (such as depth limits)
79/// before the protobuf encoding step. Types that need no pre-encode validation
80/// implement this as a no-op returning `Ok(())`.
81pub trait EncodeCheck {
82 /// Check that this model is safe to encode.
83 ///
84 /// # Errors
85 ///
86 /// Returns [`EncodeError`] if the model violates encoding constraints
87 /// (e.g., exceeds the maximum nesting depth).
88 fn check_for_encode(&self) -> Result<(), EncodeError> {
89 self.check_for_encode_from_depth(1)
90 }
91
92 /// Check that this model is safe to encode, assuming an `init` recursion
93 /// level.
94 ///
95 /// # Errors
96 ///
97 /// Returns [`EncodeError`] if the model violates encoding constraints
98 /// (e.g., exceeds the maximum nesting depth).
99 fn check_for_encode_from_depth(&self, init: usize) -> Result<(), EncodeError>;
100}
101
102/// A trait for objects that have a `try_validate` method returning `self` if the object is
103/// valid.
104pub trait TryValidate: Sized {
105 /// The type of errors returned by the validation method.
106 type Err: Display;
107 /// A validation method that returns the object itself, or an error if it is invalid.
108 ///
109 /// # Errors
110 ///
111 /// Will return errors when the implementing structure is invalid, according to its own
112 /// invariants.
113 fn try_validate(self) -> Result<Self, Self::Err>;
114}
115
116mod private {
117 use crate::api;
118 pub trait Sealed {}
119 impl Sealed for api::PolicySet {}
120 impl Sealed for api::Entities {}
121 impl Sealed for api::Entity {}
122 impl Sealed for api::Schema {}
123 impl Sealed for api::EntityTypeName {}
124 impl Sealed for api::EntityNamespace {}
125 impl Sealed for api::Template {}
126 impl Sealed for api::Expression {}
127 impl Sealed for api::Request {}
128}
129
130/// Trait allowing serializing and deserializing in protobuf format.
131pub trait Protobuf: Sized + TryValidate + private::Sealed {
132 /// Encode into protobuf format. Returns a freshly-allocated buffer containing binary data.
133 ///
134 /// # Errors
135 ///
136 /// Returns [`EncodeError::MaxDepthExceeded`] if the data structure has too many
137 /// recursion levels to be safely encoded and decoded by prost.
138 fn encode(&self) -> Result<Vec<u8>, EncodeError>;
139
140 /// Decode the binary data in `buf`, producing something of type `Self`
141 ///
142 /// # Errors
143 ///
144 /// Will return [`DecodeError::Proto`] when the input buffer does not
145 /// contain a valid protobuf message. Returns [`DecodeError::Conversion`] when
146 /// the message is well-formed but cannot be converted into the target type or the
147 /// validation on the target type failed.
148 fn decode(buf: impl prost::bytes::Buf) -> Result<Self, DecodeError> {
149 Self::decode_unchecked(buf)?
150 .try_validate()
151 .map_err(|e| ProtobufConversionError::InvalidValue(format!("invalid: {e}")).into())
152 }
153
154 /// Decode the binary data in `buf`, producing something of type `Self`,
155 /// but without the additional validation that the [`Self::decode`] method performs on the
156 /// resulting `Self`.
157 /// This is useful for performance if you can guarantee the binary data is the result of
158 /// [`Self::encode`] of the same implementation.
159 ///
160 /// # Errors
161 ///
162 /// Will return [`DecodeError::Proto`] when the input buffer does not
163 /// contain a valid protobuf message, or [`DecodeError::Conversion`] when
164 /// the message is well-formed but cannot be converted into the target type.
165 fn decode_unchecked(buf: impl prost::bytes::Buf) -> Result<Self, DecodeError>;
166}
167
168/// Encode `thing` into a caller provided buffer using the protobuf format `M`
169///
170/// # Errors
171///
172/// Returns [`EncodeError`] if the model fails pre-encode checks
173/// (e.g., expression depth exceeds [`MAX_ENCODE_DEPTH`]) or the
174/// user-provided buffer does not have enough capacity.
175#[expect(
176 dead_code,
177 reason = "experimental feature, we might have use for this one in the future"
178)]
179pub(crate) fn encode_with_buf<M: prost::Message + EncodeCheck + for<'a> From<&'a T>, T>(
180 thing: &T,
181 buf: &mut impl prost::bytes::BufMut,
182) -> Result<(), EncodeError> {
183 let model = M::from(thing);
184 model.check_for_encode()?;
185 model.encode(buf)?;
186 Ok(())
187}
188
189/// Encode `thing` into a freshly-allocated buffer using the protobuf format `M`
190///
191/// # Errors
192///
193/// Returns [`EncodeError`] if the model fails pre-encoding validation
194/// (e.g., expression depth exceeds [`MAX_ENCODE_DEPTH`]).
195pub(crate) fn encode_to_vec<M: prost::Message + EncodeCheck + for<'a> From<&'a T>, T>(
196 thing: &T,
197) -> Result<Vec<u8>, EncodeError> {
198 let model = M::from(thing);
199 model.check_for_encode()?;
200 Ok(model.encode_to_vec())
201}
202
203use std::{default::Default, fmt::Display};
204
205/// Decode something of type `T` from `buf` using the protobuf format `M`
206#[expect(
207 dead_code,
208 reason = "available for types with infallible From conversions"
209)]
210pub(crate) fn decode<M: prost::Message + Default, T: From<M>>(
211 buf: impl prost::bytes::Buf,
212) -> Result<T, DecodeError> {
213 Ok(M::decode(buf)?.into())
214}
215
216/// Decode something of type `T` from `buf` using the protobuf format `M`,
217/// where the conversion from `M` to `T` is fallible
218pub(crate) fn try_decode<
219 M: prost::Message + Default,
220 E: Into<ProtobufConversionError>,
221 T: TryFrom<M, Error = E>,
222>(
223 buf: impl prost::bytes::Buf,
224) -> Result<T, DecodeError> {
225 M::decode(buf)?
226 .try_into()
227 .map_err(|e: E| DecodeError::Conversion(e.into()))
228}
229
230// ====================================================================
231// TryValidate implementations for api types
232// ====================================================================
233
234impl TryValidate for api::PolicySet {
235 type Err = PolicySetValidationError;
236 fn try_validate(self) -> Result<Self, Self::Err> {
237 self.ast.try_validate().map(Into::into)
238 }
239}
240
241impl TryValidate for api::Entities {
242 type Err = EntitiesError;
243 fn try_validate(self) -> Result<Self, EntitiesError> {
244 Ok(Self(self.0.try_validate()?))
245 }
246}
247
248impl TryValidate for api::Entity {
249 type Err = EntitiesError;
250 fn try_validate(self) -> Result<Self, EntitiesError> {
251 Ok(Self(self.0.try_validate()?))
252 }
253}
254
255impl TryValidate for api::Schema {
256 type Err = SchemaError;
257 fn try_validate(self) -> Result<Self, SchemaError> {
258 Ok(Self(self.0.try_validate()?))
259 }
260}
261
262impl TryValidate for api::Template {
263 type Err = TemplateValidationError;
264 fn try_validate(self) -> Result<Self, TemplateValidationError> {
265 Ok(Self {
266 ast: self.ast.try_validate()?,
267 ..self
268 })
269 }
270}
271
272impl TryValidate for api::Expression {
273 type Err = ExprValidationError;
274 fn try_validate(self) -> Result<Self, ExprValidationError> {
275 Ok(Self(self.0.try_validate()?))
276 }
277}
278
279impl TryValidate for api::Request {
280 type Err = Infallible;
281 fn try_validate(self) -> Result<Self, Infallible> {
282 // We don't actually do any additional validation on requests, the structural validation enforced
283 // by types and the existing conversion is sufficient.
284 Ok(self)
285 }
286}
287
288impl TryValidate for api::EntityTypeName {
289 type Err = Infallible;
290 fn try_validate(self) -> Result<Self, Infallible> {
291 // EntityTypeName also doesn't need additional validation
292 Ok(self)
293 }
294}
295
296impl TryValidate for api::EntityNamespace {
297 type Err = Infallible;
298 fn try_validate(self) -> Result<Self, Infallible> {
299 // EntityNamespace also doesn't need additional validation
300 Ok(self)
301 }
302}
303
304// ====================================================================
305// EncodeCheck implementations for protobuf model types.
306// Encoding checks are implemented on the protobuf model type rather than
307// the AST/Validator level type to get a more predictable outcome: it is easier to
308// know how much recursion is needed to decode the protobuf representation
309// at this level than at the expression/type level.
310// ====================================================================
311
312use super::models;
313
314impl EncodeCheck for models::Expr {
315 #[expect(
316 clippy::too_many_lines,
317 reason = "many variants in expr and more readable that way"
318 )]
319 fn check_for_encode_from_depth(&self, init: usize) -> Result<(), EncodeError> {
320 // Iterative depth-first traversal measuring protobuf recursion depth.
321 let mut stack: Vec<(&Self, usize)> = vec![(self, init)];
322
323 while let Some((expr, depth)) = stack.pop() {
324 if depth > MAX_ENCODE_DEPTH {
325 return Err(EncodeError::MaxDepthExceeded);
326 }
327
328 if let Some(ref kind) = expr.expr_kind {
329 use models::expr::ExprKind;
330 // Prost counts each message entry as one recursion level. For our Expr
331 // schema, entering an Expr costs 1 level, and entering the variant
332 // wrapper message (BinaryApp, UnaryApp, If, etc.) costs another 1 level.
333 let child_depth = depth + 2;
334 match kind {
335 ExprKind::Lit(lit) => {
336 // A literal EUid adds 3: (Literal (EntityUuid (Name ...))
337 if let Some(models::expr::literal::Lit::Euid(_)) = lit.lit {
338 let euid_name_depth = depth + 3;
339 if euid_name_depth > MAX_ENCODE_DEPTH {
340 return Err(EncodeError::MaxDepthExceeded);
341 }
342 }
343 }
344 ExprKind::Var(_) | ExprKind::Slot(_) => {}
345 ExprKind::If(if_expr) => {
346 if let Some(ref e) = if_expr.test_expr {
347 stack.push((e, child_depth));
348 }
349 if let Some(ref e) = if_expr.then_expr {
350 stack.push((e, child_depth));
351 }
352 if let Some(ref e) = if_expr.else_expr {
353 stack.push((e, child_depth));
354 }
355 }
356 ExprKind::And(bin) => {
357 if let Some(ref e) = bin.left {
358 stack.push((e, child_depth));
359 }
360 if let Some(ref e) = bin.right {
361 stack.push((e, child_depth));
362 }
363 }
364 ExprKind::Or(bin) => {
365 if let Some(ref e) = bin.left {
366 stack.push((e, child_depth));
367 }
368 if let Some(ref e) = bin.right {
369 stack.push((e, child_depth));
370 }
371 }
372 ExprKind::UApp(unary) => {
373 if let Some(ref e) = unary.expr {
374 stack.push((e, child_depth));
375 }
376 }
377 ExprKind::BApp(binary) => {
378 if let Some(ref e) = binary.left {
379 stack.push((e, child_depth));
380 }
381 if let Some(ref e) = binary.right {
382 stack.push((e, child_depth));
383 }
384 }
385 ExprKind::ExtApp(ext) => {
386 for arg in &ext.args {
387 stack.push((arg, child_depth));
388 }
389 // a `fn_name` adds 2 : (ExtApp(Name(..)).
390 if ext.fn_name.is_some() {
391 let name_depth = depth + 2;
392 if name_depth > MAX_ENCODE_DEPTH {
393 return Err(EncodeError::MaxDepthExceeded);
394 }
395 }
396 }
397 ExprKind::GetAttr(get) => {
398 if let Some(ref e) = get.expr {
399 stack.push((e, child_depth));
400 }
401 }
402 ExprKind::HasAttr(has) => {
403 if let Some(ref e) = has.expr {
404 stack.push((e, child_depth));
405 }
406 }
407 ExprKind::Like(like) => {
408 if let Some(ref e) = like.expr {
409 stack.push((e, child_depth));
410 }
411 // PatternElem adds 2 (Like(Vec<PatternElem>(..))
412 if !like.pattern.is_empty() {
413 let pattern_depth = depth + 2;
414 if pattern_depth > MAX_ENCODE_DEPTH {
415 return Err(EncodeError::MaxDepthExceeded);
416 }
417 }
418 }
419 ExprKind::Is(is) => {
420 if let Some(ref e) = is.expr {
421 stack.push((e, child_depth));
422 }
423 // `entity_type` adds 2 (Is (Name (..)))
424 if is.entity_type.is_some() {
425 let name_depth = depth + 2;
426 if name_depth > MAX_ENCODE_DEPTH {
427 return Err(EncodeError::MaxDepthExceeded);
428 }
429 }
430 }
431 ExprKind::Set(set) => {
432 for elem in &set.elements {
433 stack.push((elem, child_depth));
434 }
435 }
436 ExprKind::Record(record) => {
437 // map<string, Expr> adds an extra map-entry wrapper message:
438 // Record (+1) → map entry (+1) → Expr (+1) = +3 from parent Expr
439 let record_child_depth = depth + 3;
440 for value in record.items.values() {
441 stack.push((value, record_child_depth));
442 }
443 }
444 }
445 }
446 }
447 Ok(())
448 }
449}
450
451impl EncodeCheck for models::Entity {
452 fn check_for_encode_from_depth(&self, init: usize) -> Result<(), EncodeError> {
453 // Validate all attribute and tag expressions
454 for expr in self.attrs.values().chain(self.tags.values()) {
455 // Map: +1 for entering the list, +1 for entering the map entry
456 expr.check_for_encode_from_depth(init + 2)?;
457 }
458 Ok(())
459 }
460}
461
462impl EncodeCheck for models::Entities {
463 fn check_for_encode_from_depth(&self, init: usize) -> Result<(), EncodeError> {
464 for entity in &self.entities {
465 entity.check_for_encode_from_depth(init + 1)?;
466 }
467 Ok(())
468 }
469}
470
471impl EncodeCheck for models::TemplateBody {
472 fn check_for_encode_from_depth(&self, init: usize) -> Result<(), EncodeError> {
473 // Validate the non-scope constraint expression within the template body
474 if let Some(ref expr) = self.non_scope_constraints {
475 // List: +1 for entering the list
476 expr.check_for_encode_from_depth(init + 1)?;
477 }
478 Ok(())
479 }
480}
481
482impl EncodeCheck for models::PolicySet {
483 fn check_for_encode_from_depth(&self, init: usize) -> Result<(), EncodeError> {
484 for template in &self.templates {
485 // List: +1 for entering the list
486 template.check_for_encode_from_depth(init + 1)?;
487 }
488 Ok(())
489 }
490}
491
492/// Trivial `EncodeCheck` for model types that have no recursive structure
493/// requiring depth checks.
494impl EncodeCheck for models::Name {
495 fn check_for_encode_from_depth(&self, _init: usize) -> Result<(), EncodeError> {
496 Ok(())
497 }
498}
499
500impl EncodeCheck for models::Request {
501 fn check_for_encode_from_depth(&self, init: usize) -> Result<(), EncodeError> {
502 // The context field contains expressions that may be deeply nested
503 for expr in self.context.values() {
504 // Map: +1 for entering the list, +1 for entering the map entry
505 expr.check_for_encode_from_depth(init + 2)?;
506 }
507 Ok(())
508 }
509}
510
511impl EncodeCheck for models::Schema {
512 fn check_for_encode_from_depth(&self, init: usize) -> Result<(), EncodeError> {
513 for entity_decl in &self.entity_decls {
514 // + 1 for entering entity decls
515 for attr_type in entity_decl.attributes.values() {
516 // Map: +1 for entering the list, +1 for entering the map entry
517 check_type_depth(attr_type, init + 3)?;
518 }
519 if let Some(ref tag_type) = entity_decl.tags {
520 // Map: +1 for entering the list, +1 for entering the map entry
521 check_type_depth_inner(tag_type, init + 3)?;
522 }
523 }
524 for action_decl in &self.action_decls {
525 // + 1 for entering action decls list
526 for attr_type in action_decl.context.values() {
527 // Map: +1 for entering the list, +1 for entering the map entry
528 check_type_depth(attr_type, init + 3)?;
529 }
530 }
531 Ok(())
532 }
533}
534
535/// Validate that an [`AttributeType`](models::AttributeType) doesn't exceed
536/// the prost recursion budget.
537///
538/// Prost recursion cost from an `AttributeType`:
539/// +1 (`AttributeType` message) + cost of inner `Type`
540fn check_type_depth(attr_type: &models::AttributeType, init: usize) -> Result<(), EncodeError> {
541 if let Some(ref ty) = attr_type.attr_type {
542 // AttributeType itself costs 1 prost level, then the Type inside costs 1 more
543 check_type_depth_inner(ty, init + 2)?;
544 }
545 Ok(())
546}
547
548/// Iteratively check `Type` nesting depth in prost recursion terms.
549///
550/// Prost recursion costs per `Type` variant:
551/// - `SetElem(Box<Type>)`: the child `Type` is directly nested → +1 per level
552/// - `Record`: +1 (`Record` message) + 1 (`AttributeType` message) + 1 (child `Type`) = +3
553/// - Primitive/Entity/Extension: terminal, no recursion
554fn check_type_depth_inner(root: &models::Type, starting_depth: usize) -> Result<(), EncodeError> {
555 // Stack of (Type, prost_depth)
556 let mut stack: Vec<(&models::Type, usize)> = vec![(root, starting_depth)];
557
558 while let Some((ty, depth)) = stack.pop() {
559 if depth > MAX_ENCODE_DEPTH {
560 return Err(EncodeError::MaxDepthExceeded);
561 }
562
563 if let Some(ref data) = ty.data {
564 use models::r#type::Data;
565 match data {
566 Data::Prim(_) => {}
567 Data::Entity(_) | Data::Ext(_) => {
568 // Entity/Ext adds 1: (Name(..))
569 let name_depth = depth + 1;
570 if name_depth > MAX_ENCODE_DEPTH {
571 return Err(EncodeError::MaxDepthExceeded);
572 }
573 }
574 Data::SetElem(inner_type) => {
575 // set_elem is directly a Type field: +1 prost level
576 stack.push((inner_type, depth + 1));
577 }
578 Data::Record(record) => {
579 // Record message (+1) -> map entry (+1) -> AttributeType (+1) -> Type (+1) = +4
580 for attr_type in record.attrs.values() {
581 if let Some(ref inner_ty) = attr_type.attr_type {
582 stack.push((inner_ty, depth + 4));
583 }
584 }
585 }
586 }
587 }
588 }
589 Ok(())
590}