Skip to main content

cedar_policy/proto/
policy.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#![allow(clippy::use_self, reason = "readability")]
18
19use super::ast::ProtobufConversionError;
20use super::models;
21use cedar_policy_core::{ast, FromNormalizedStr};
22use std::collections::HashMap;
23
24impl TryFrom<models::Policy> for ast::LiteralPolicy {
25    type Error = ProtobufConversionError;
26    fn try_from(v: models::Policy) -> Result<Self, Self::Error> {
27        let mut values: ast::SlotEnv = HashMap::new();
28        if let Some(principal_euid) = v.principal_euid {
29            values.insert(
30                ast::SlotId::principal(),
31                ast::EntityUID::try_from(principal_euid)?,
32            );
33        }
34        if let Some(resource_euid) = v.resource_euid {
35            values.insert(
36                ast::SlotId::resource(),
37                ast::EntityUID::try_from(resource_euid)?,
38            );
39        }
40
41        let template_id = ast::PolicyID::from_string(v.template_id);
42
43        if v.is_template_link {
44            Ok(Self::template_linked_policy(
45                template_id,
46                ast::PolicyID::from_string(
47                    v.link_id
48                        .as_ref()
49                        .ok_or_else(|| ProtobufConversionError::missing("link_id"))?,
50                ),
51                values,
52            ))
53        } else {
54            Ok(Self::static_policy(template_id))
55        }
56    }
57}
58
59impl From<&ast::LiteralPolicy> for models::Policy {
60    fn from(v: &ast::LiteralPolicy) -> Self {
61        Self {
62            template_id: v.template_id().as_ref().to_string(),
63            link_id: if v.is_static() {
64                None
65            } else {
66                Some(v.id().as_ref().to_string())
67            },
68            is_template_link: !v.is_static(),
69            principal_euid: v
70                .value(&ast::SlotId::principal())
71                .map(models::EntityUid::from),
72            resource_euid: v
73                .value(&ast::SlotId::resource())
74                .map(models::EntityUid::from),
75        }
76    }
77}
78
79impl From<&ast::Policy> for models::Policy {
80    fn from(v: &ast::Policy) -> Self {
81        Self {
82            template_id: v.template().id().as_ref().to_string(),
83            link_id: if v.is_static() {
84                None
85            } else {
86                Some(v.id().as_ref().to_string())
87            },
88            is_template_link: !v.is_static(),
89            principal_euid: v
90                .env()
91                .get(&ast::SlotId::principal())
92                .map(models::EntityUid::from),
93            resource_euid: v
94                .env()
95                .get(&ast::SlotId::resource())
96                .map(models::EntityUid::from),
97        }
98    }
99}
100
101impl TryFrom<models::TemplateBody> for ast::Template {
102    type Error = ProtobufConversionError;
103    fn try_from(v: models::TemplateBody) -> Result<Self, Self::Error> {
104        Ok(ast::Template::from(ast::TemplateBody::try_from(v)?))
105    }
106}
107
108impl TryFrom<models::TemplateBody> for ast::TemplateBody {
109    type Error = ProtobufConversionError;
110    fn try_from(v: models::TemplateBody) -> Result<Self, Self::Error> {
111        let effect = models::Effect::try_from(v.effect)
112            .map_err(|e| ProtobufConversionError::InvalidValue(format!("invalid effect: {e}")))?;
113        Ok(ast::TemplateBody::new(
114            ast::PolicyID::from_string(v.id),
115            None,
116            v.annotations
117                .into_iter()
118                .map(|(key, value)| {
119                    ast::AnyId::from_normalized_str(&key)
120                        .map(|k| {
121                            (
122                                k,
123                                ast::Annotation {
124                                    val: value.into(),
125                                    loc: None,
126                                },
127                            )
128                        })
129                        .map_err(|e| {
130                            ProtobufConversionError::InvalidValue(format!(
131                                "invalid annotation key `{key}`: {e}"
132                            ))
133                        })
134                })
135                .collect::<Result<_, _>>()?,
136            ast::Effect::from(effect),
137            ast::PrincipalConstraint::try_from(
138                v.principal_constraint
139                    .ok_or_else(|| ProtobufConversionError::missing("principal_constraint"))?,
140            )?,
141            ast::ActionConstraint::try_from(
142                v.action_constraint
143                    .ok_or_else(|| ProtobufConversionError::missing("action_constraint"))?,
144            )?,
145            ast::ResourceConstraint::try_from(
146                v.resource_constraint
147                    .ok_or_else(|| ProtobufConversionError::missing("resource_constraint"))?,
148            )?,
149            v.non_scope_constraints
150                .map(ast::Expr::try_from)
151                .transpose()?,
152        ))
153    }
154}
155
156impl From<&ast::TemplateBody> for models::TemplateBody {
157    fn from(v: &ast::TemplateBody) -> Self {
158        let annotations: HashMap<String, String> = v
159            .annotations()
160            .map(|(key, value)| (key.as_ref().into(), value.as_ref().into()))
161            .collect();
162
163        Self {
164            id: v.id().as_ref().to_string(),
165            annotations,
166            effect: models::Effect::from(&v.effect()).into(),
167            principal_constraint: Some(models::PrincipalOrResourceConstraint::from(
168                v.principal_constraint(),
169            )),
170            action_constraint: Some(models::ActionConstraint::from(v.action_constraint())),
171            resource_constraint: Some(models::PrincipalOrResourceConstraint::from(
172                v.resource_constraint(),
173            )),
174            non_scope_constraints: v.non_scope_constraints().map(models::Expr::from),
175        }
176    }
177}
178
179impl From<&ast::Template> for models::TemplateBody {
180    fn from(v: &ast::Template) -> Self {
181        models::TemplateBody::from(&ast::TemplateBody::from(v.clone()))
182    }
183}
184
185impl TryFrom<models::PrincipalOrResourceConstraint> for ast::PrincipalConstraint {
186    type Error = ProtobufConversionError;
187    fn try_from(v: models::PrincipalOrResourceConstraint) -> Result<Self, Self::Error> {
188        Ok(Self::new(ast::PrincipalOrResourceConstraint::try_from(v)?))
189    }
190}
191
192impl From<&ast::PrincipalConstraint> for models::PrincipalOrResourceConstraint {
193    fn from(v: &ast::PrincipalConstraint) -> Self {
194        models::PrincipalOrResourceConstraint::from(v.as_inner())
195    }
196}
197
198impl TryFrom<models::PrincipalOrResourceConstraint> for ast::ResourceConstraint {
199    type Error = ProtobufConversionError;
200    fn try_from(v: models::PrincipalOrResourceConstraint) -> Result<Self, Self::Error> {
201        Ok(Self::new(ast::PrincipalOrResourceConstraint::try_from(v)?))
202    }
203}
204
205impl From<&ast::ResourceConstraint> for models::PrincipalOrResourceConstraint {
206    fn from(v: &ast::ResourceConstraint) -> Self {
207        models::PrincipalOrResourceConstraint::from(v.as_inner())
208    }
209}
210
211impl TryFrom<models::EntityReference> for ast::EntityReference {
212    type Error = ProtobufConversionError;
213    fn try_from(v: models::EntityReference) -> Result<Self, Self::Error> {
214        match v
215            .data
216            .ok_or_else(|| ProtobufConversionError::missing("data"))?
217        {
218            models::entity_reference::Data::Slot(slot) => {
219                match models::entity_reference::Slot::try_from(slot).map_err(|e| {
220                    ProtobufConversionError::InvalidValue(format!(
221                        "invalid entity reference slot: {e}"
222                    ))
223                })? {
224                    models::entity_reference::Slot::Unit => Ok(ast::EntityReference::Slot(None)),
225                }
226            }
227            models::entity_reference::Data::Euid(euid) => Ok(ast::EntityReference::euid(
228                ast::EntityUID::try_from(euid)?.into(),
229            )),
230        }
231    }
232}
233
234impl From<&ast::EntityReference> for models::EntityReference {
235    fn from(v: &ast::EntityReference) -> Self {
236        match v {
237            ast::EntityReference::EUID(euid) => Self {
238                data: Some(models::entity_reference::Data::Euid(
239                    models::EntityUid::from(euid.as_ref()),
240                )),
241            },
242            ast::EntityReference::Slot(_) => Self {
243                data: Some(models::entity_reference::Data::Slot(
244                    models::entity_reference::Slot::Unit.into(),
245                )),
246            },
247        }
248    }
249}
250
251impl TryFrom<models::PrincipalOrResourceConstraint> for ast::PrincipalOrResourceConstraint {
252    type Error = ProtobufConversionError;
253    fn try_from(v: models::PrincipalOrResourceConstraint) -> Result<Self, Self::Error> {
254        match v
255            .data
256            .ok_or_else(|| ProtobufConversionError::missing("data"))?
257        {
258            models::principal_or_resource_constraint::Data::Any(unit) => {
259                match models::principal_or_resource_constraint::Any::try_from(unit).map_err(
260                    |e| {
261                        ProtobufConversionError::InvalidValue(format!(
262                            "invalid principal/resource constraint: {e}"
263                        ))
264                    },
265                )? {
266                    models::principal_or_resource_constraint::Any::Unit => {
267                        Ok(ast::PrincipalOrResourceConstraint::Any)
268                    }
269                }
270            }
271            models::principal_or_resource_constraint::Data::In(msg) => Ok(
272                ast::PrincipalOrResourceConstraint::In(ast::EntityReference::try_from(
273                    msg.er
274                        .ok_or_else(|| ProtobufConversionError::missing("er"))?,
275                )?),
276            ),
277            models::principal_or_resource_constraint::Data::Eq(msg) => Ok(
278                ast::PrincipalOrResourceConstraint::Eq(ast::EntityReference::try_from(
279                    msg.er
280                        .ok_or_else(|| ProtobufConversionError::missing("er"))?,
281                )?),
282            ),
283            models::principal_or_resource_constraint::Data::Is(msg) => {
284                Ok(ast::PrincipalOrResourceConstraint::Is(
285                    ast::EntityType::try_from(
286                        msg.entity_type
287                            .ok_or_else(|| ProtobufConversionError::missing("entity_type"))?,
288                    )?
289                    .into(),
290                ))
291            }
292            models::principal_or_resource_constraint::Data::IsIn(msg) => {
293                Ok(ast::PrincipalOrResourceConstraint::IsIn(
294                    ast::EntityType::try_from(
295                        msg.entity_type
296                            .ok_or_else(|| ProtobufConversionError::missing("entity_type"))?,
297                    )?
298                    .into(),
299                    ast::EntityReference::try_from(
300                        msg.er
301                            .ok_or_else(|| ProtobufConversionError::missing("er"))?,
302                    )?,
303                ))
304            }
305        }
306    }
307}
308
309impl From<&ast::PrincipalOrResourceConstraint> for models::PrincipalOrResourceConstraint {
310    fn from(v: &ast::PrincipalOrResourceConstraint) -> Self {
311        match v {
312            ast::PrincipalOrResourceConstraint::Any => Self {
313                data: Some(models::principal_or_resource_constraint::Data::Any(
314                    models::principal_or_resource_constraint::Any::Unit.into(),
315                )),
316            },
317            ast::PrincipalOrResourceConstraint::In(er) => Self {
318                data: Some(models::principal_or_resource_constraint::Data::In(
319                    models::principal_or_resource_constraint::InMessage {
320                        er: Some(models::EntityReference::from(er)),
321                    },
322                )),
323            },
324            ast::PrincipalOrResourceConstraint::Eq(er) => Self {
325                data: Some(models::principal_or_resource_constraint::Data::Eq(
326                    models::principal_or_resource_constraint::EqMessage {
327                        er: Some(models::EntityReference::from(er)),
328                    },
329                )),
330            },
331            ast::PrincipalOrResourceConstraint::Is(na) => Self {
332                data: Some(models::principal_or_resource_constraint::Data::Is(
333                    models::principal_or_resource_constraint::IsMessage {
334                        entity_type: Some(models::Name::from(na.as_ref())),
335                    },
336                )),
337            },
338            ast::PrincipalOrResourceConstraint::IsIn(na, er) => Self {
339                data: Some(models::principal_or_resource_constraint::Data::IsIn(
340                    models::principal_or_resource_constraint::IsInMessage {
341                        er: Some(models::EntityReference::from(er)),
342                        entity_type: Some(models::Name::from(na.as_ref())),
343                    },
344                )),
345            },
346        }
347    }
348}
349
350impl TryFrom<models::ActionConstraint> for ast::ActionConstraint {
351    type Error = ProtobufConversionError;
352    fn try_from(v: models::ActionConstraint) -> Result<Self, Self::Error> {
353        match v
354            .data
355            .ok_or_else(|| ProtobufConversionError::missing("data"))?
356        {
357            models::action_constraint::Data::Any(unit) => {
358                match models::action_constraint::Any::try_from(unit).map_err(|e| {
359                    ProtobufConversionError::InvalidValue(format!("invalid action constraint: {e}"))
360                })? {
361                    models::action_constraint::Any::Unit => Ok(ast::ActionConstraint::Any),
362                }
363            }
364            models::action_constraint::Data::In(msg) => Ok(ast::ActionConstraint::In(
365                msg.euids
366                    .into_iter()
367                    .map(|value| Ok(ast::EntityUID::try_from(value)?.into()))
368                    .collect::<Result<_, ProtobufConversionError>>()?,
369            )),
370            models::action_constraint::Data::Eq(msg) => Ok(ast::ActionConstraint::Eq(
371                ast::EntityUID::try_from(
372                    msg.euid
373                        .ok_or_else(|| ProtobufConversionError::missing("euid"))?,
374                )?
375                .into(),
376            )),
377        }
378    }
379}
380
381impl From<&ast::ActionConstraint> for models::ActionConstraint {
382    fn from(v: &ast::ActionConstraint) -> Self {
383        match v {
384            ast::ActionConstraint::Any => Self {
385                data: Some(models::action_constraint::Data::Any(
386                    models::action_constraint::Any::Unit.into(),
387                )),
388            },
389            ast::ActionConstraint::In(euids) => {
390                let mut peuids: Vec<models::EntityUid> = Vec::with_capacity(euids.len());
391                for value in euids {
392                    peuids.push(models::EntityUid::from(value.as_ref()));
393                }
394                Self {
395                    data: Some(models::action_constraint::Data::In(
396                        models::action_constraint::InMessage { euids: peuids },
397                    )),
398                }
399            }
400            ast::ActionConstraint::Eq(euid) => Self {
401                data: Some(models::action_constraint::Data::Eq(
402                    models::action_constraint::EqMessage {
403                        euid: Some(models::EntityUid::from(euid.as_ref())),
404                    },
405                )),
406            },
407            #[cfg(feature = "tolerant-ast")]
408            #[expect(clippy::unimplemented, reason = "experimental feature")]
409            ast::ActionConstraint::ErrorConstraint => {
410                unimplemented!("tolerant-ast cannot be used with the protobuf feature")
411            }
412        }
413    }
414}
415
416impl From<models::Effect> for ast::Effect {
417    fn from(v: models::Effect) -> Self {
418        match v {
419            models::Effect::Forbid => ast::Effect::Forbid,
420            models::Effect::Permit => ast::Effect::Permit,
421        }
422    }
423}
424
425impl From<&ast::Effect> for models::Effect {
426    fn from(v: &ast::Effect) -> Self {
427        match v {
428            ast::Effect::Permit => models::Effect::Permit,
429            ast::Effect::Forbid => models::Effect::Forbid,
430        }
431    }
432}
433
434impl TryFrom<models::PolicySet> for ast::LiteralPolicySet {
435    type Error = ProtobufConversionError;
436    fn try_from(v: models::PolicySet) -> Result<Self, Self::Error> {
437        let templates = v
438            .templates
439            .into_iter()
440            .map(|tb| {
441                let id = ast::PolicyID::from_string(&tb.id);
442                Ok((id, ast::Template::from(ast::TemplateBody::try_from(tb)?)))
443            })
444            .collect::<Result<Vec<_>, ProtobufConversionError>>()?;
445
446        let links = v
447            .links
448            .into_iter()
449            .map(|p| {
450                // per docs in core.proto, for static policies, `link_id` is omitted/ignored,
451                // and the ID of the policy is the `template_id`.
452                let id = if p.is_template_link {
453                    p.link_id
454                        .as_ref()
455                        .ok_or_else(|| ProtobufConversionError::missing("link_id"))?
456                } else {
457                    &p.template_id
458                };
459                let id = ast::PolicyID::from_string(id);
460                Ok((id, ast::LiteralPolicy::try_from(p)?))
461            })
462            .collect::<Result<Vec<_>, ProtobufConversionError>>()?;
463
464        Ok(Self::new(templates, links))
465    }
466}
467
468impl From<&ast::LiteralPolicySet> for models::PolicySet {
469    fn from(v: &ast::LiteralPolicySet) -> Self {
470        let templates = v.templates().map(models::TemplateBody::from).collect();
471        let links = v.policies().map(models::Policy::from).collect();
472        Self { templates, links }
473    }
474}
475
476impl From<&ast::PolicySet> for models::PolicySet {
477    fn from(v: &ast::PolicySet) -> Self {
478        let templates = v.all_templates().map(models::TemplateBody::from).collect();
479        let links = v.policies().map(models::Policy::from).collect();
480        Self { templates, links }
481    }
482}
483
484impl TryFrom<models::PolicySet> for ast::PolicySet {
485    type Error = ProtobufConversionError;
486    fn try_from(pset: models::PolicySet) -> Result<Self, Self::Error> {
487        let literal = ast::LiteralPolicySet::try_from(pset)?;
488        ast::PolicySet::try_from(literal)
489            .map_err(|e| ProtobufConversionError::InvalidValue(format!("invalid policy set: {e}")))
490    }
491}
492
493#[cfg(test)]
494mod test {
495    use std::sync::Arc;
496
497    use super::*;
498    use cool_asserts::assert_matches;
499
500    // We add `PartialOrd` and `Ord` implementations for both `models::Policy` and
501    // `models::TemplateBody`, so that these can be sorted for testing purposes
502    impl PartialOrd for models::Policy {
503        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
504            Some(self.cmp(other))
505        }
506    }
507    impl Ord for models::Policy {
508        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
509            // assumes that (link-id, template-id) pair is unique, otherwise we're
510            // technically violating `Ord` contract because there could exist two
511            // policies that return `Ordering::Equal` but are not equal with `Eq`
512            self.link_id()
513                .cmp(other.link_id())
514                .then_with(|| self.template_id.cmp(&other.template_id))
515        }
516    }
517    impl Eq for models::TemplateBody {}
518    impl PartialOrd for models::TemplateBody {
519        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
520            Some(self.cmp(other))
521        }
522    }
523    impl Ord for models::TemplateBody {
524        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
525            // assumes that IDs are unique, otherwise we're technically violating
526            // `Ord` contract because there could exist two template-bodies that
527            // return `Ordering::Equal` but are not equal with `Eq`
528            self.id.cmp(&other.id)
529        }
530    }
531
532    #[test]
533    #[expect(clippy::too_many_lines, reason = "unit test code")]
534    fn policy_roundtrip() {
535        let annotation1 = ast::Annotation {
536            val: "".into(),
537            loc: None,
538        };
539
540        let annotation2 = ast::Annotation {
541            val: "Hello World".into(),
542            loc: None,
543        };
544
545        assert_eq!(
546            ast::Effect::Permit,
547            ast::Effect::from(models::Effect::from(&ast::Effect::Permit))
548        );
549        assert_eq!(
550            ast::Effect::Forbid,
551            ast::Effect::from(models::Effect::from(&ast::Effect::Forbid))
552        );
553
554        let er1 = ast::EntityReference::euid(Arc::new(
555            ast::EntityUID::with_eid_and_type("A", "foo").unwrap(),
556        ));
557        assert_eq!(
558            er1,
559            ast::EntityReference::try_from(models::EntityReference::from(&er1)).unwrap()
560        );
561        assert_eq!(
562            ast::EntityReference::Slot(None),
563            ast::EntityReference::try_from(models::EntityReference::from(
564                &ast::EntityReference::Slot(None)
565            ))
566            .unwrap()
567        );
568
569        let read_euid = Arc::new(ast::EntityUID::with_eid_and_type("Action", "read").unwrap());
570        let write_euid = Arc::new(ast::EntityUID::with_eid_and_type("Action", "write").unwrap());
571        let ac1 = ast::ActionConstraint::Eq(read_euid.clone());
572        let ac2 = ast::ActionConstraint::In(vec![read_euid, write_euid]);
573        assert_eq!(
574            ast::ActionConstraint::Any,
575            ast::ActionConstraint::try_from(models::ActionConstraint::from(
576                &ast::ActionConstraint::Any
577            ))
578            .unwrap()
579        );
580        assert_eq!(
581            ac1,
582            ast::ActionConstraint::try_from(models::ActionConstraint::from(&ac1)).unwrap()
583        );
584        assert_eq!(
585            ac2,
586            ast::ActionConstraint::try_from(models::ActionConstraint::from(&ac2)).unwrap()
587        );
588
589        let euid1 = Arc::new(ast::EntityUID::with_eid_and_type("A", "friend").unwrap());
590        let name1 = Arc::new(ast::EntityType::from(
591            ast::Name::from_normalized_str("B::C::D").unwrap(),
592        ));
593        let prc1 = ast::PrincipalOrResourceConstraint::is_eq(euid1.clone());
594        let prc2 = ast::PrincipalOrResourceConstraint::is_in(euid1.clone());
595        let prc3 = ast::PrincipalOrResourceConstraint::is_entity_type(name1.clone());
596        let prc4 = ast::PrincipalOrResourceConstraint::is_entity_type_in(name1, euid1);
597        assert_eq!(
598            ast::PrincipalOrResourceConstraint::any(),
599            ast::PrincipalOrResourceConstraint::try_from(
600                models::PrincipalOrResourceConstraint::from(
601                    &ast::PrincipalOrResourceConstraint::any()
602                )
603            )
604            .unwrap()
605        );
606        assert_eq!(
607            prc1,
608            ast::PrincipalOrResourceConstraint::try_from(
609                models::PrincipalOrResourceConstraint::from(&prc1)
610            )
611            .unwrap()
612        );
613        assert_eq!(
614            prc2,
615            ast::PrincipalOrResourceConstraint::try_from(
616                models::PrincipalOrResourceConstraint::from(&prc2)
617            )
618            .unwrap()
619        );
620        assert_eq!(
621            prc3,
622            ast::PrincipalOrResourceConstraint::try_from(
623                models::PrincipalOrResourceConstraint::from(&prc3)
624            )
625            .unwrap()
626        );
627        assert_eq!(
628            prc4,
629            ast::PrincipalOrResourceConstraint::try_from(
630                models::PrincipalOrResourceConstraint::from(&prc4)
631            )
632            .unwrap()
633        );
634
635        let pc = ast::PrincipalConstraint::new(prc1);
636        let rc = ast::ResourceConstraint::new(prc3);
637        assert_eq!(
638            pc,
639            ast::PrincipalConstraint::try_from(models::PrincipalOrResourceConstraint::from(&pc))
640                .unwrap()
641        );
642        assert_eq!(
643            rc,
644            ast::ResourceConstraint::try_from(models::PrincipalOrResourceConstraint::from(&rc))
645                .unwrap()
646        );
647
648        assert_eq!(
649            ast::Effect::Permit,
650            ast::Effect::from(models::Effect::from(&ast::Effect::Permit))
651        );
652        assert_eq!(
653            ast::Effect::Forbid,
654            ast::Effect::from(models::Effect::from(&ast::Effect::Forbid))
655        );
656
657        let tb = ast::TemplateBody::new(
658            ast::PolicyID::from_string("template"),
659            None,
660            ast::Annotations::from_iter([
661                (
662                    ast::AnyId::from_normalized_str("read").unwrap(),
663                    annotation1,
664                ),
665                (
666                    ast::AnyId::from_normalized_str("write").unwrap(),
667                    annotation2,
668                ),
669            ]),
670            ast::Effect::Permit,
671            pc.clone(),
672            ac1.clone(),
673            rc.clone(),
674            None,
675        );
676        assert_eq!(
677            tb,
678            ast::TemplateBody::try_from(models::TemplateBody::from(&tb)).unwrap()
679        );
680
681        let policy = ast::LiteralPolicy::template_linked_policy(
682            ast::PolicyID::from_string("template"),
683            ast::PolicyID::from_string("id"),
684            HashMap::from_iter([(
685                ast::SlotId::principal(),
686                ast::EntityUID::with_eid_and_type("A", "eid").unwrap(),
687            )]),
688        );
689        assert_eq!(
690            policy,
691            ast::LiteralPolicy::try_from(models::Policy::from(&policy)).unwrap()
692        );
693
694        let tb = ast::TemplateBody::new(
695            ast::PolicyID::from_string("\0\n \' \"+-$^!"),
696            None,
697            ast::Annotations::from_iter([]),
698            ast::Effect::Permit,
699            pc,
700            ac1,
701            rc,
702            None,
703        );
704        assert_eq!(
705            tb,
706            ast::TemplateBody::try_from(models::TemplateBody::from(&tb)).unwrap()
707        );
708
709        let policy = ast::LiteralPolicy::template_linked_policy(
710            ast::PolicyID::from_string("template\0\n \' \"+-$^!"),
711            ast::PolicyID::from_string("link\0\n \' \"+-$^!"),
712            HashMap::from_iter([(
713                ast::SlotId::principal(),
714                ast::EntityUID::with_eid_and_type("A", "eid").unwrap(),
715            )]),
716        );
717        assert_eq!(
718            policy,
719            ast::LiteralPolicy::try_from(models::Policy::from(&policy)).unwrap()
720        );
721    }
722
723    #[test]
724    fn policyset_roundtrip() {
725        let tb = ast::TemplateBody::new(
726            ast::PolicyID::from_string("template"),
727            None,
728            ast::Annotations::from_iter(vec![(
729                ast::AnyId::from_normalized_str("read").unwrap(),
730                ast::Annotation {
731                    val: "".into(),
732                    loc: None,
733                },
734            )]),
735            ast::Effect::Permit,
736            ast::PrincipalConstraint::is_eq_slot(),
737            ast::ActionConstraint::Eq(
738                ast::EntityUID::with_eid_and_type("Action", "read")
739                    .unwrap()
740                    .into(),
741            ),
742            ast::ResourceConstraint::is_entity_type(
743                ast::EntityType::from(ast::Name::from_normalized_str("photo").unwrap()).into(),
744            ),
745            None,
746        );
747
748        let policy1 = ast::Policy::from_when_clause(
749            ast::Effect::Permit,
750            ast::Expr::val(true),
751            ast::PolicyID::from_string("permit-true-trivial"),
752            None,
753        );
754        let policy2 = ast::Policy::from_when_clause(
755            ast::Effect::Forbid,
756            ast::Expr::is_eq(
757                ast::Expr::var(ast::Var::Principal),
758                ast::Expr::val(ast::EntityUID::with_eid_and_type("A", "dog").unwrap()),
759            ),
760            ast::PolicyID::from_string("forbid-dog"),
761            None,
762        );
763
764        let mut ps = ast::PolicySet::new();
765        ps.add_template(ast::Template::from(tb))
766            .expect("Failed to add template to policy set.");
767        ps.add(policy1).expect("Failed to add policy to policy set");
768        ps.add(policy2).expect("Failed to add policy to policy set");
769        ps.link(
770            ast::PolicyID::from_string("template"),
771            ast::PolicyID::from_string("link"),
772            HashMap::from_iter([(
773                ast::SlotId::principal(),
774                ast::EntityUID::with_eid_and_type("A", "friend").unwrap(),
775            )]),
776        )
777        .unwrap();
778        let mut mps = models::PolicySet::from(&ps);
779        let mut mps_roundtrip =
780            models::PolicySet::from(&ast::LiteralPolicySet::try_from(mps.clone()).unwrap());
781
782        // we accept permutations as equivalent, so before comparison, we sort
783        // both `.templates` and `.links`
784        mps.templates.sort();
785        mps_roundtrip.templates.sort();
786        mps.links.sort();
787        mps_roundtrip.links.sort();
788
789        // Can't compare `models::PolicySet` directly, so we compare their fields
790        assert_eq!(mps.templates, mps_roundtrip.templates);
791        assert_eq!(mps.links, mps_roundtrip.links);
792    }
793
794    #[test]
795    fn policyset_roundtrip_escapes() {
796        let tb = ast::TemplateBody::new(
797            ast::PolicyID::from_string("template\0\n \' \"+-$^!"),
798            None,
799            ast::Annotations::from_iter(vec![(
800                ast::AnyId::from_normalized_str("read").unwrap(),
801                ast::Annotation {
802                    val: "".into(),
803                    loc: None,
804                },
805            )]),
806            ast::Effect::Permit,
807            ast::PrincipalConstraint::is_eq_slot(),
808            ast::ActionConstraint::Eq(
809                ast::EntityUID::with_eid_and_type("Action", "read")
810                    .unwrap()
811                    .into(),
812            ),
813            ast::ResourceConstraint::is_entity_type(
814                ast::EntityType::from(ast::Name::from_normalized_str("photo").unwrap()).into(),
815            ),
816            None,
817        );
818
819        let policy1 = ast::Policy::from_when_clause(
820            ast::Effect::Permit,
821            ast::Expr::val(true),
822            ast::PolicyID::from_string("permit-true-trivial\0\n \' \"+-$^!"),
823            None,
824        );
825        let policy2 = ast::Policy::from_when_clause(
826            ast::Effect::Forbid,
827            ast::Expr::is_eq(
828                ast::Expr::var(ast::Var::Principal),
829                ast::Expr::val(ast::EntityUID::with_eid_and_type("A", "dog").unwrap()),
830            ),
831            ast::PolicyID::from_string("forbid-dog\0\n \' \"+-$^!"),
832            None,
833        );
834
835        let mut ps = ast::PolicySet::new();
836        ps.add_template(ast::Template::from(tb))
837            .expect("Failed to add template to policy set.");
838        ps.add(policy1).expect("Failed to add policy to policy set");
839        ps.add(policy2).expect("Failed to add policy to policy set");
840        ps.link(
841            ast::PolicyID::from_string("template\0\n \' \"+-$^!"),
842            ast::PolicyID::from_string("link\0\n \' \"+-$^!"),
843            HashMap::from_iter([(
844                ast::SlotId::principal(),
845                ast::EntityUID::with_eid_and_type("A", "friend").unwrap(),
846            )]),
847        )
848        .unwrap();
849        let mut mps = models::PolicySet::from(&ps);
850        let mut mps_roundtrip =
851            models::PolicySet::from(&ast::LiteralPolicySet::try_from(mps.clone()).unwrap());
852
853        // we accept permutations as equivalent, so before comparison, we sort
854        // both `.templates` and `.links`
855        mps.templates.sort();
856        mps_roundtrip.templates.sort();
857        mps.links.sort();
858        mps_roundtrip.links.sort();
859
860        // Can't compare `models::PolicySet` directly, so we compare their fields
861        assert_eq!(mps.templates, mps_roundtrip.templates);
862        assert_eq!(mps.links, mps_roundtrip.links);
863    }
864
865    #[test]
866    fn template_body_try_from_missing_principal_constraint() {
867        let bad = models::TemplateBody {
868            id: "t".to_string(),
869            annotations: Default::default(),
870            effect: models::Effect::Permit.into(),
871            principal_constraint: None,
872            action_constraint: Some(models::ActionConstraint {
873                data: Some(models::action_constraint::Data::Any(
874                    models::action_constraint::Any::Unit.into(),
875                )),
876            }),
877            resource_constraint: Some(models::PrincipalOrResourceConstraint {
878                data: Some(models::principal_or_resource_constraint::Data::Any(
879                    models::principal_or_resource_constraint::Any::Unit.into(),
880                )),
881            }),
882            non_scope_constraints: None,
883        };
884        assert_matches!(
885            ast::TemplateBody::try_from(bad),
886            Err(ProtobufConversionError::MissingField(f)) if f == "principal_constraint"
887        );
888    }
889
890    #[test]
891    fn template_body_try_from_invalid_annotation_key() {
892        let bad = models::TemplateBody {
893            id: "t".to_string(),
894            annotations: [("".to_string(), "v".to_string())].into_iter().collect(),
895            effect: models::Effect::Permit.into(),
896            principal_constraint: Some(models::PrincipalOrResourceConstraint {
897                data: Some(models::principal_or_resource_constraint::Data::Any(
898                    models::principal_or_resource_constraint::Any::Unit.into(),
899                )),
900            }),
901            action_constraint: Some(models::ActionConstraint {
902                data: Some(models::action_constraint::Data::Any(
903                    models::action_constraint::Any::Unit.into(),
904                )),
905            }),
906            resource_constraint: Some(models::PrincipalOrResourceConstraint {
907                data: Some(models::principal_or_resource_constraint::Data::Any(
908                    models::principal_or_resource_constraint::Any::Unit.into(),
909                )),
910            }),
911            non_scope_constraints: None,
912        };
913        assert_matches!(
914            ast::TemplateBody::try_from(bad),
915            Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("invalid annotation key")
916        );
917    }
918
919    #[test]
920    fn entity_reference_try_from_missing_data() {
921        let bad = models::EntityReference { data: None };
922        assert_matches!(
923            ast::EntityReference::try_from(bad),
924            Err(ProtobufConversionError::MissingField(f)) if f == "data"
925        );
926    }
927
928    #[test]
929    fn principal_or_resource_constraint_try_from_missing_data() {
930        let bad = models::PrincipalOrResourceConstraint { data: None };
931        assert_matches!(
932            ast::PrincipalOrResourceConstraint::try_from(bad),
933            Err(ProtobufConversionError::MissingField(f)) if f == "data"
934        );
935    }
936
937    #[test]
938    fn principal_or_resource_constraint_try_from_in_missing_er() {
939        let bad = models::PrincipalOrResourceConstraint {
940            data: Some(models::principal_or_resource_constraint::Data::In(
941                models::principal_or_resource_constraint::InMessage { er: None },
942            )),
943        };
944        assert_matches!(
945            ast::PrincipalOrResourceConstraint::try_from(bad),
946            Err(ProtobufConversionError::MissingField(f)) if f == "er"
947        );
948    }
949
950    #[test]
951    fn principal_or_resource_constraint_try_from_eq_missing_er() {
952        let bad = models::PrincipalOrResourceConstraint {
953            data: Some(models::principal_or_resource_constraint::Data::Eq(
954                models::principal_or_resource_constraint::EqMessage { er: None },
955            )),
956        };
957        assert_matches!(
958            ast::PrincipalOrResourceConstraint::try_from(bad),
959            Err(ProtobufConversionError::MissingField(f)) if f == "er"
960        );
961    }
962
963    #[test]
964    fn principal_or_resource_constraint_try_from_is_missing_entity_type() {
965        let bad = models::PrincipalOrResourceConstraint {
966            data: Some(models::principal_or_resource_constraint::Data::Is(
967                models::principal_or_resource_constraint::IsMessage { entity_type: None },
968            )),
969        };
970        assert_matches!(
971            ast::PrincipalOrResourceConstraint::try_from(bad),
972            Err(ProtobufConversionError::MissingField(f)) if f == "entity_type"
973        );
974    }
975
976    #[test]
977    fn principal_or_resource_constraint_try_from_is_in_missing_entity_type() {
978        let bad = models::PrincipalOrResourceConstraint {
979            data: Some(models::principal_or_resource_constraint::Data::IsIn(
980                models::principal_or_resource_constraint::IsInMessage {
981                    entity_type: None,
982                    er: None,
983                },
984            )),
985        };
986        assert_matches!(
987            ast::PrincipalOrResourceConstraint::try_from(bad),
988            Err(ProtobufConversionError::MissingField(f)) if f == "entity_type"
989        );
990    }
991
992    #[test]
993    fn action_constraint_try_from_missing_data() {
994        let bad = models::ActionConstraint { data: None };
995        assert_matches!(
996            ast::ActionConstraint::try_from(bad),
997            Err(ProtobufConversionError::MissingField(f)) if f == "data"
998        );
999    }
1000
1001    #[test]
1002    fn action_constraint_try_from_eq_missing_euid() {
1003        let bad = models::ActionConstraint {
1004            data: Some(models::action_constraint::Data::Eq(
1005                models::action_constraint::EqMessage { euid: None },
1006            )),
1007        };
1008        assert_matches!(
1009            ast::ActionConstraint::try_from(bad),
1010            Err(ProtobufConversionError::MissingField(f)) if f == "euid"
1011        );
1012    }
1013
1014    #[test]
1015    fn literal_policy_try_from_template_link_missing_link_id() {
1016        let bad = models::Policy {
1017            template_id: "t".to_string(),
1018            link_id: None,
1019            is_template_link: true,
1020            principal_euid: None,
1021            resource_euid: None,
1022        };
1023        assert_matches!(
1024            ast::LiteralPolicy::try_from(bad),
1025            Err(ProtobufConversionError::MissingField(f)) if f == "link_id"
1026        );
1027    }
1028
1029    #[test]
1030    fn literal_policy_set_try_from_link_missing_link_id() {
1031        let bad = models::PolicySet {
1032            templates: vec![models::TemplateBody {
1033                id: "t".to_string(),
1034                annotations: Default::default(),
1035                effect: models::Effect::Permit.into(),
1036                principal_constraint: Some(models::PrincipalOrResourceConstraint {
1037                    data: Some(models::principal_or_resource_constraint::Data::Any(
1038                        models::principal_or_resource_constraint::Any::Unit.into(),
1039                    )),
1040                }),
1041                action_constraint: Some(models::ActionConstraint {
1042                    data: Some(models::action_constraint::Data::Any(
1043                        models::action_constraint::Any::Unit.into(),
1044                    )),
1045                }),
1046                resource_constraint: Some(models::PrincipalOrResourceConstraint {
1047                    data: Some(models::principal_or_resource_constraint::Data::Any(
1048                        models::principal_or_resource_constraint::Any::Unit.into(),
1049                    )),
1050                }),
1051                non_scope_constraints: None,
1052            }],
1053            links: vec![models::Policy {
1054                template_id: "t".to_string(),
1055                link_id: None,
1056                is_template_link: true,
1057                principal_euid: None,
1058                resource_euid: None,
1059            }],
1060        };
1061        assert_matches!(
1062            ast::LiteralPolicySet::try_from(bad),
1063            Err(ProtobufConversionError::MissingField(f)) if f == "link_id"
1064        );
1065    }
1066}