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 super::super::api;
18use super::{ast::ProtobufConversionError, models, traits};
19
20/// Macro that implements `From<A>` and `TryFrom<B>` for types where
21/// one conversion direction is infallible, the other is not. This is typically the case where
22/// the API type converts to protobuf models without failing, but converting the protobuf model
23/// to the API type requires additional checks.
24macro_rules! fallible_conversions {
25    ( $A:ty, $A_expr:expr, $B:ty ) => {
26        impl From<&$A> for $B {
27            fn from(v: &$A) -> $B {
28                Self::from(&v.0)
29            }
30        }
31
32        impl TryFrom<$B> for $A {
33            type Error = ProtobufConversionError;
34            fn try_from(v: $B) -> Result<$A, Self::Error> {
35                Ok($A_expr(v.try_into()?))
36            }
37        }
38    };
39}
40
41// fallible conversions (encode infallible, decode fallible)
42
43fallible_conversions!(api::Entity, api::Entity, models::Entity);
44fallible_conversions!(api::EntityUid, api::EntityUid, models::EntityUid);
45fallible_conversions!(api::Entities, api::Entities, models::Entities);
46fallible_conversions!(api::Schema, api::Schema, models::Schema);
47fallible_conversions!(api::EntityTypeName, api::EntityTypeName, models::Name);
48fallible_conversions!(api::EntityNamespace, api::EntityNamespace, models::Name);
49fallible_conversions!(api::Expression, api::Expression, models::Expr);
50fallible_conversions!(api::Request, api::Request, models::Request);
51
52// nonstandard conversions
53
54impl From<&api::Template> for models::TemplateBody {
55    fn from(v: &api::Template) -> Self {
56        Self::from(&v.ast)
57    }
58}
59
60impl TryFrom<models::TemplateBody> for api::Template {
61    type Error = ProtobufConversionError;
62    fn try_from(v: models::TemplateBody) -> Result<Self, Self::Error> {
63        Ok(Self::from_ast(v.try_into()?))
64    }
65}
66
67impl From<&api::Policy> for models::Policy {
68    fn from(v: &api::Policy) -> Self {
69        Self::from(&v.ast)
70    }
71}
72
73impl From<&api::PolicySet> for models::PolicySet {
74    fn from(v: &api::PolicySet) -> Self {
75        Self::from(&v.ast)
76    }
77}
78
79impl TryFrom<models::PolicySet> for api::PolicySet {
80    type Error = ProtobufConversionError;
81    fn try_from(v: models::PolicySet) -> Result<Self, Self::Error> {
82        let ast: cedar_policy_core::ast::PolicySet = v.try_into()?;
83        Self::from_ast(ast)
84            .map_err(|e| ProtobufConversionError::InvalidValue(format!("invalid policy set: {e}")))
85    }
86}
87
88/// Macro that implements `traits::Protobuf` for cases where `From<>` and `TryFrom<>`
89/// conversions exist between the api type `$api` and the protobuf model type `$model`
90macro_rules! standard_protobuf_impl {
91    ( $api:ty, $model:ty ) => {
92        impl traits::Protobuf for $api {
93            fn encode(&self) -> Vec<u8> {
94                traits::encode_to_vec::<$model>(self)
95            }
96            fn decode(buf: impl prost::bytes::Buf) -> Result<Self, traits::DecodeError> {
97                traits::try_decode::<$model, _, _>(buf)
98            }
99        }
100    };
101}
102
103// standard implementations of `traits::Protobuf`
104
105standard_protobuf_impl!(api::Entity, models::Entity);
106standard_protobuf_impl!(api::Entities, models::Entities);
107standard_protobuf_impl!(api::Schema, models::Schema);
108standard_protobuf_impl!(api::EntityTypeName, models::Name);
109standard_protobuf_impl!(api::EntityNamespace, models::Name);
110standard_protobuf_impl!(api::Template, models::TemplateBody);
111standard_protobuf_impl!(api::Expression, models::Expr);
112standard_protobuf_impl!(api::Request, models::Request);
113
114// nonstandard implementations of `traits::Protobuf`
115
116impl traits::Protobuf for api::PolicySet {
117    fn encode(&self) -> Vec<u8> {
118        traits::encode_to_vec::<models::PolicySet>(self)
119    }
120    fn decode(buf: impl prost::bytes::Buf) -> Result<Self, traits::DecodeError> {
121        traits::try_decode::<models::PolicySet, _, Self>(buf)
122    }
123}
124
125#[cfg(test)]
126mod test {
127    use cool_asserts::assert_matches;
128    use prost::Message as _;
129    use std::{collections::HashMap, str::FromStr};
130
131    /// Performs a series of conversions: API -> Protobuf model -> Protobuf bytes -> Protobuf model -> API.
132    /// Checks that the input API policy set is equal to the converted policy set.
133    fn roundtrip_policies(policies: crate::PolicySet) {
134        // API -> Protobuf model
135        let policies_proto = crate::proto::models::PolicySet::from(&policies);
136        // Protobuf model -> Protobuf bytes
137        let buf = policies_proto.encode_to_vec();
138        // Protobuf bytes -> Protobuf model
139        let roundtripped_proto = crate::proto::models::PolicySet::decode(&buf[..])
140            .expect("Failed to deserialize PolicySet from protobuf");
141        // -> Protobuf model -> API
142        let roundtripped = crate::PolicySet::try_from(roundtripped_proto)
143            .expect("Failed to convert from protobuf to PolicySet");
144        similar_asserts::assert_eq!(policies, roundtripped);
145    }
146
147    fn roundtrip_policies_text(text: &str) {
148        let pset = crate::PolicySet::from_str(text).expect("Failed to parse policy set");
149        roundtrip_policies(pset);
150    }
151
152    #[test]
153    fn roundtrip_policyset_with_template_link() {
154        let mut pset = crate::PolicySet::from_str(
155            r#"
156            permit(principal == ?principal, action, resource);
157            "#,
158        )
159        .expect("Failed to parse policy set");
160        pset.link(
161            crate::PolicyId::new("policy0"),
162            crate::PolicyId::new("link0"),
163            HashMap::from([(
164                crate::SlotId::principal(),
165                crate::EntityUid::from_strs("User", "alice"),
166            )]),
167        )
168        .expect("Failed to link template");
169        roundtrip_policies(pset);
170    }
171
172    #[test]
173    fn roundtrip_policyset_empty() {
174        roundtrip_policies_text("");
175    }
176
177    #[test]
178    fn roundtrip_policyset_with_static_policy() {
179        roundtrip_policies_text(
180            r#"
181            permit(principal, action, resource);
182            "#,
183        );
184    }
185
186    #[test]
187    fn roundtrip_policyset_with_multiple_static_policies() {
188        roundtrip_policies_text(
189            r#"
190            permit(principal, action, resource);
191
192            forbid(principal, action, resource) when { context.is_restricted };
193
194            permit(principal == User::"alice", action == Action::"read", resource in Folder::"shared");
195            "#,
196        );
197    }
198
199    #[test]
200    fn roundtrip_policyset_with_when_and_unless() {
201        roundtrip_policies_text(
202            r#"
203            permit(principal, action, resource)
204                when { resource.owner == principal }
205                unless { principal.suspended };
206            "#,
207        );
208    }
209
210    #[test]
211    fn roundtrip_policyset_with_annotations() {
212        roundtrip_policies_text(
213            r#"
214            @advice("allow owner access")
215            permit(principal, action == Action::"write", resource)
216            when { resource.owner == principal };
217            "#,
218        );
219    }
220
221    #[test]
222    fn roundtrip_policyset_with_multiple_template_links() {
223        let mut pset = crate::PolicySet::from_str(
224            r#"
225            permit(principal == ?principal, action, resource in ?resource);
226            "#,
227        )
228        .expect("Failed to parse policy set");
229        pset.link(
230            crate::PolicyId::new("policy0"),
231            crate::PolicyId::new("link0"),
232            HashMap::from([
233                (
234                    crate::SlotId::principal(),
235                    crate::EntityUid::from_strs("User", "alice"),
236                ),
237                (
238                    crate::SlotId::resource(),
239                    crate::EntityUid::from_strs("Folder", "shared"),
240                ),
241            ]),
242        )
243        .expect("Failed to link template");
244        pset.link(
245            crate::PolicyId::new("policy0"),
246            crate::PolicyId::new("link1"),
247            HashMap::from([
248                (
249                    crate::SlotId::principal(),
250                    crate::EntityUid::from_strs("User", "bob"),
251                ),
252                (
253                    crate::SlotId::resource(),
254                    crate::EntityUid::from_strs("Folder", "private"),
255                ),
256            ]),
257        )
258        .expect("Failed to link template");
259        roundtrip_policies(pset);
260    }
261
262    #[test]
263    fn roundtrip_policyset_with_static_and_templates() {
264        let mut pset = crate::PolicySet::from_str(
265            r#"
266            forbid(principal, action, resource) unless { context.authenticated };
267
268            permit(principal == ?principal, action, resource);
269            "#,
270        )
271        .expect("Failed to parse policy set");
272        println!("{:?}", pset);
273        pset.link(
274            crate::PolicyId::new("policy1"),
275            crate::PolicyId::new("link0"),
276            HashMap::from([(
277                crate::SlotId::principal(),
278                crate::EntityUid::from_strs("User", "admin"),
279            )]),
280        )
281        .expect("Failed to link template");
282        roundtrip_policies(pset);
283    }
284
285    #[test]
286    fn roundtrip_policyset_with_is_constraint() {
287        roundtrip_policies_text(
288            r#"
289            permit(principal is User, action, resource is Folder);
290            "#,
291        );
292    }
293
294    #[test]
295    fn roundtrip_policyset_with_is_in_constraint() {
296        roundtrip_policies_text(
297            r#"
298            permit(principal is User in Group::"admins", action, resource);
299            "#,
300        );
301    }
302
303    #[test]
304    fn roundtrip_policyset_with_action_in_set() {
305        roundtrip_policies_text(
306            r#"
307            permit(principal, action in [Action::"read", Action::"list"], resource);
308            "#,
309        );
310    }
311
312    #[test]
313    fn roundtrip_policyset_with_extension_functions() {
314        roundtrip_policies_text(
315            r#"
316            forbid(principal, action, resource)
317                when { !context.src_ip.isInRange(ip("10.0.0.0/8")) };
318            "#,
319        );
320    }
321
322    #[test]
323    fn roundtrip_policyset_with_unlinked_template() {
324        roundtrip_policies_text(
325            r#"
326            permit(principal == ?principal, action, resource);
327            "#,
328        );
329    }
330
331    /// Decoding arbitrary bytes must never panic — it should return `Err`.
332    #[test]
333    fn decode_random_bytes_does_not_panic() {
334        use crate::proto::traits::Protobuf;
335
336        let inputs: &[&[u8]] = &[
337            b"",
338            b"\x00",
339            b"\xff\xff\xff\xff",
340            b"not a protobuf",
341            &[0u8; 1024],
342            &{
343                let mut v = Vec::new();
344                for i in 0u8..=255 {
345                    v.push(i);
346                }
347                v
348            },
349        ];
350
351        for input in inputs {
352            let _ = crate::Entity::decode(*input);
353            let _ = crate::Entities::decode(*input);
354            let _ = crate::Schema::decode(*input);
355            let _ = crate::EntityTypeName::decode(*input);
356            let _ = crate::EntityNamespace::decode(*input);
357            let _ = crate::Template::decode(*input);
358            let _ = crate::Expression::decode(*input);
359            let _ = crate::Request::decode(*input);
360            let _ = crate::PolicySet::decode(*input);
361        }
362    }
363
364    #[test]
365    fn decode_conversion_error_path() {
366        use crate::proto::traits::Protobuf;
367        // An Entity with a uid whose type name is empty string triggers
368        // ProtobufConversionError, exercising the DecodeError::Conversion path.
369        let model = crate::proto::models::Entity {
370            uid: Some(crate::proto::models::EntityUid {
371                ty: Some(crate::proto::models::Name {
372                    id: String::new(), // invalid: empty identifier
373                    path: vec![],
374                }),
375                eid: "x".to_string(),
376            }),
377            attrs: Default::default(),
378            ancestors: vec![],
379            tags: Default::default(),
380        };
381        let buf = prost::Message::encode_to_vec(&model);
382        assert_matches!(
383            crate::Entity::decode(&buf[..]),
384            Err(crate::proto::traits::DecodeError::Conversion(_))
385        );
386    }
387}