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::models;
20use cedar_policy_core::{ast, FromNormalizedStr};
21use std::collections::HashMap;
22
23impl From<&models::Policy> for ast::LiteralPolicy {
24    fn from(v: &models::Policy) -> Self {
25        let mut values: ast::SlotEnv = HashMap::new();
26        if let Some(principal_euid) = v.principal_euid.as_ref() {
27            values.insert(
28                ast::SlotId::principal(),
29                ast::EntityUID::from(principal_euid),
30            );
31        }
32        if let Some(resource_euid) = v.resource_euid.as_ref() {
33            values.insert(ast::SlotId::resource(), ast::EntityUID::from(resource_euid));
34        }
35
36        let template_id = ast::PolicyID::from_string(v.template_id.clone());
37
38        if v.is_template_link {
39            #[expect(clippy::expect_used, reason = "experimental feature")]
40            Self::template_linked_policy(
41                template_id,
42                ast::PolicyID::from_string(v.link_id.as_ref().expect("link_id field should exist")),
43                values,
44            )
45        } else {
46            Self::static_policy(template_id)
47        }
48    }
49}
50
51impl From<&ast::LiteralPolicy> for models::Policy {
52    fn from(v: &ast::LiteralPolicy) -> Self {
53        Self {
54            template_id: v.template_id().as_ref().to_string(),
55            link_id: if v.is_static() {
56                None
57            } else {
58                Some(v.id().as_ref().to_string())
59            },
60            is_template_link: !v.is_static(),
61            principal_euid: v
62                .value(&ast::SlotId::principal())
63                .map(models::EntityUid::from),
64            resource_euid: v
65                .value(&ast::SlotId::resource())
66                .map(models::EntityUid::from),
67        }
68    }
69}
70
71impl From<&ast::Policy> for models::Policy {
72    fn from(v: &ast::Policy) -> Self {
73        Self {
74            template_id: v.template().id().as_ref().to_string(),
75            link_id: if v.is_static() {
76                None
77            } else {
78                Some(v.id().as_ref().to_string())
79            },
80            is_template_link: !v.is_static(),
81            principal_euid: v
82                .env()
83                .get(&ast::SlotId::principal())
84                .map(models::EntityUid::from),
85            resource_euid: v
86                .env()
87                .get(&ast::SlotId::resource())
88                .map(models::EntityUid::from),
89        }
90    }
91}
92
93impl From<&models::TemplateBody> for ast::Template {
94    fn from(v: &models::TemplateBody) -> Self {
95        ast::Template::from(ast::TemplateBody::from(v))
96    }
97}
98
99impl From<&models::TemplateBody> for ast::TemplateBody {
100    #[expect(
101        clippy::expect_used,
102        clippy::unwrap_used,
103        reason = "experimental feature"
104    )]
105    fn from(v: &models::TemplateBody) -> Self {
106        ast::TemplateBody::new(
107            ast::PolicyID::from_string(v.id.clone()),
108            None,
109            v.annotations
110                .iter()
111                .map(|(key, value)| {
112                    (
113                        ast::AnyId::from_normalized_str(key).unwrap(),
114                        ast::Annotation {
115                            val: value.into(),
116                            loc: None,
117                        },
118                    )
119                })
120                .collect(),
121            ast::Effect::from(&models::Effect::try_from(v.effect).expect("decode should succeed")),
122            ast::PrincipalConstraint::from(
123                v.principal_constraint
124                    .as_ref()
125                    .expect("principal_constraint field should exist"),
126            ),
127            ast::ActionConstraint::from(
128                v.action_constraint
129                    .as_ref()
130                    .expect("action_constraint field should exist"),
131            ),
132            ast::ResourceConstraint::from(
133                v.resource_constraint
134                    .as_ref()
135                    .expect("resource_constraint field should exist"),
136            ),
137            v.non_scope_constraints.as_ref().map(ast::Expr::from),
138        )
139    }
140}
141
142impl From<&ast::TemplateBody> for models::TemplateBody {
143    fn from(v: &ast::TemplateBody) -> Self {
144        let annotations: HashMap<String, String> = v
145            .annotations()
146            .map(|(key, value)| (key.as_ref().into(), value.as_ref().into()))
147            .collect();
148
149        Self {
150            id: v.id().as_ref().to_string(),
151            annotations,
152            effect: models::Effect::from(&v.effect()).into(),
153            principal_constraint: Some(models::PrincipalOrResourceConstraint::from(
154                v.principal_constraint(),
155            )),
156            action_constraint: Some(models::ActionConstraint::from(v.action_constraint())),
157            resource_constraint: Some(models::PrincipalOrResourceConstraint::from(
158                v.resource_constraint(),
159            )),
160            non_scope_constraints: v.non_scope_constraints().map(models::Expr::from),
161        }
162    }
163}
164
165impl From<&ast::Template> for models::TemplateBody {
166    fn from(v: &ast::Template) -> Self {
167        models::TemplateBody::from(&ast::TemplateBody::from(v.clone()))
168    }
169}
170
171impl From<&models::PrincipalOrResourceConstraint> for ast::PrincipalConstraint {
172    fn from(v: &models::PrincipalOrResourceConstraint) -> Self {
173        Self::new(ast::PrincipalOrResourceConstraint::from(v))
174    }
175}
176
177impl From<&ast::PrincipalConstraint> for models::PrincipalOrResourceConstraint {
178    fn from(v: &ast::PrincipalConstraint) -> Self {
179        models::PrincipalOrResourceConstraint::from(v.as_inner())
180    }
181}
182
183impl From<&models::PrincipalOrResourceConstraint> for ast::ResourceConstraint {
184    fn from(v: &models::PrincipalOrResourceConstraint) -> Self {
185        Self::new(ast::PrincipalOrResourceConstraint::from(v))
186    }
187}
188
189impl From<&ast::ResourceConstraint> for models::PrincipalOrResourceConstraint {
190    fn from(v: &ast::ResourceConstraint) -> Self {
191        models::PrincipalOrResourceConstraint::from(v.as_inner())
192    }
193}
194
195impl From<&models::EntityReference> for ast::EntityReference {
196    #[expect(clippy::expect_used, reason = "experimental feature")]
197    fn from(v: &models::EntityReference) -> Self {
198        match v.data.as_ref().expect("data field should exist") {
199            models::entity_reference::Data::Slot(slot) => {
200                match models::entity_reference::Slot::try_from(*slot)
201                    .expect("decode should succeed")
202                {
203                    models::entity_reference::Slot::Unit => ast::EntityReference::Slot(None),
204                }
205            }
206            models::entity_reference::Data::Euid(euid) => {
207                ast::EntityReference::euid(ast::EntityUID::from(euid).into())
208            }
209        }
210    }
211}
212
213impl From<&ast::EntityReference> for models::EntityReference {
214    fn from(v: &ast::EntityReference) -> Self {
215        match v {
216            ast::EntityReference::EUID(euid) => Self {
217                data: Some(models::entity_reference::Data::Euid(
218                    models::EntityUid::from(euid.as_ref()),
219                )),
220            },
221            ast::EntityReference::Slot(_) => Self {
222                data: Some(models::entity_reference::Data::Slot(
223                    models::entity_reference::Slot::Unit.into(),
224                )),
225            },
226        }
227    }
228}
229
230impl From<&models::PrincipalOrResourceConstraint> for ast::PrincipalOrResourceConstraint {
231    #[expect(clippy::expect_used, reason = "experimental feature")]
232    fn from(v: &models::PrincipalOrResourceConstraint) -> Self {
233        match v.data.as_ref().expect("data field should exist") {
234            models::principal_or_resource_constraint::Data::Any(unit) => {
235                match models::principal_or_resource_constraint::Any::try_from(*unit)
236                    .expect("decode should succeed")
237                {
238                    models::principal_or_resource_constraint::Any::Unit => {
239                        ast::PrincipalOrResourceConstraint::Any
240                    }
241                }
242            }
243            models::principal_or_resource_constraint::Data::In(msg) => {
244                ast::PrincipalOrResourceConstraint::In(ast::EntityReference::from(
245                    msg.er.as_ref().expect("er field should exist"),
246                ))
247            }
248            models::principal_or_resource_constraint::Data::Eq(msg) => {
249                ast::PrincipalOrResourceConstraint::Eq(ast::EntityReference::from(
250                    msg.er.as_ref().expect("er field should exist"),
251                ))
252            }
253            models::principal_or_resource_constraint::Data::Is(msg) => {
254                ast::PrincipalOrResourceConstraint::Is(
255                    ast::EntityType::from(
256                        msg.entity_type
257                            .as_ref()
258                            .expect("entity_type field should exist"),
259                    )
260                    .into(),
261                )
262            }
263            models::principal_or_resource_constraint::Data::IsIn(msg) => {
264                ast::PrincipalOrResourceConstraint::IsIn(
265                    ast::EntityType::from(
266                        msg.entity_type
267                            .as_ref()
268                            .expect("entity_type field should exist"),
269                    )
270                    .into(),
271                    ast::EntityReference::from(msg.er.as_ref().expect("er field should exist")),
272                )
273            }
274        }
275    }
276}
277
278impl From<&ast::PrincipalOrResourceConstraint> for models::PrincipalOrResourceConstraint {
279    fn from(v: &ast::PrincipalOrResourceConstraint) -> Self {
280        match v {
281            ast::PrincipalOrResourceConstraint::Any => Self {
282                data: Some(models::principal_or_resource_constraint::Data::Any(
283                    models::principal_or_resource_constraint::Any::Unit.into(),
284                )),
285            },
286            ast::PrincipalOrResourceConstraint::In(er) => Self {
287                data: Some(models::principal_or_resource_constraint::Data::In(
288                    models::principal_or_resource_constraint::InMessage {
289                        er: Some(models::EntityReference::from(er)),
290                    },
291                )),
292            },
293            ast::PrincipalOrResourceConstraint::Eq(er) => Self {
294                data: Some(models::principal_or_resource_constraint::Data::Eq(
295                    models::principal_or_resource_constraint::EqMessage {
296                        er: Some(models::EntityReference::from(er)),
297                    },
298                )),
299            },
300            ast::PrincipalOrResourceConstraint::Is(na) => Self {
301                data: Some(models::principal_or_resource_constraint::Data::Is(
302                    models::principal_or_resource_constraint::IsMessage {
303                        entity_type: Some(models::Name::from(na.as_ref())),
304                    },
305                )),
306            },
307            ast::PrincipalOrResourceConstraint::IsIn(na, er) => Self {
308                data: Some(models::principal_or_resource_constraint::Data::IsIn(
309                    models::principal_or_resource_constraint::IsInMessage {
310                        er: Some(models::EntityReference::from(er)),
311                        entity_type: Some(models::Name::from(na.as_ref())),
312                    },
313                )),
314            },
315        }
316    }
317}
318
319impl From<&models::ActionConstraint> for ast::ActionConstraint {
320    #[expect(clippy::expect_used, reason = "experimental feature")]
321    fn from(v: &models::ActionConstraint) -> Self {
322        match v.data.as_ref().expect("data.as_ref()") {
323            models::action_constraint::Data::Any(unit) => {
324                match models::action_constraint::Any::try_from(*unit)
325                    .expect("decode should succeed")
326                {
327                    models::action_constraint::Any::Unit => ast::ActionConstraint::Any,
328                }
329            }
330            models::action_constraint::Data::In(msg) => ast::ActionConstraint::In(
331                msg.euids
332                    .iter()
333                    .map(|value| ast::EntityUID::from(value).into())
334                    .collect(),
335            ),
336            models::action_constraint::Data::Eq(msg) => ast::ActionConstraint::Eq(
337                ast::EntityUID::from(msg.euid.as_ref().expect("euid field should exist")).into(),
338            ),
339        }
340    }
341}
342
343impl From<&ast::ActionConstraint> for models::ActionConstraint {
344    fn from(v: &ast::ActionConstraint) -> Self {
345        match v {
346            ast::ActionConstraint::Any => Self {
347                data: Some(models::action_constraint::Data::Any(
348                    models::action_constraint::Any::Unit.into(),
349                )),
350            },
351            ast::ActionConstraint::In(euids) => {
352                let mut peuids: Vec<models::EntityUid> = Vec::with_capacity(euids.len());
353                for value in euids {
354                    peuids.push(models::EntityUid::from(value.as_ref()));
355                }
356                Self {
357                    data: Some(models::action_constraint::Data::In(
358                        models::action_constraint::InMessage { euids: peuids },
359                    )),
360                }
361            }
362            ast::ActionConstraint::Eq(euid) => Self {
363                data: Some(models::action_constraint::Data::Eq(
364                    models::action_constraint::EqMessage {
365                        euid: Some(models::EntityUid::from(euid.as_ref())),
366                    },
367                )),
368            },
369            #[cfg(feature = "tolerant-ast")]
370            ast::ActionConstraint::ErrorConstraint =>
371            // Treat an error constraint as an Any constraint for Protobufs since Protobufs schema model has no Error
372            {
373                Self {
374                    data: Some(models::action_constraint::Data::Any(
375                        models::action_constraint::Any::Unit.into(),
376                    )),
377                }
378            }
379        }
380    }
381}
382
383impl From<&models::Effect> for ast::Effect {
384    fn from(v: &models::Effect) -> Self {
385        match v {
386            models::Effect::Forbid => ast::Effect::Forbid,
387            models::Effect::Permit => ast::Effect::Permit,
388        }
389    }
390}
391
392impl From<&ast::Effect> for models::Effect {
393    fn from(v: &ast::Effect) -> Self {
394        match v {
395            ast::Effect::Permit => models::Effect::Permit,
396            ast::Effect::Forbid => models::Effect::Forbid,
397        }
398    }
399}
400
401impl From<&models::PolicySet> for ast::LiteralPolicySet {
402    #[expect(clippy::expect_used, reason = "experimental feature")]
403    fn from(v: &models::PolicySet) -> Self {
404        let templates = v.templates.iter().map(|tb| {
405            (
406                ast::PolicyID::from_string(&tb.id),
407                ast::Template::from(ast::TemplateBody::from(tb)),
408            )
409        });
410
411        let links = v.links.iter().map(|p| {
412            // per docs in core.proto, for static policies, `link_id` is omitted/ignored,
413            // and the ID of the policy is the `template_id`.
414            let id = if p.is_template_link {
415                p.link_id
416                    .as_ref()
417                    .expect("template link should have a link_id")
418            } else {
419                &p.template_id
420            };
421            (ast::PolicyID::from_string(id), ast::LiteralPolicy::from(p))
422        });
423
424        Self::new(templates, links)
425    }
426}
427
428impl From<&ast::LiteralPolicySet> for models::PolicySet {
429    fn from(v: &ast::LiteralPolicySet) -> Self {
430        let templates = v.templates().map(models::TemplateBody::from).collect();
431        let links = v.policies().map(models::Policy::from).collect();
432        Self { templates, links }
433    }
434}
435
436impl From<&ast::PolicySet> for models::PolicySet {
437    fn from(v: &ast::PolicySet) -> Self {
438        let templates = v.all_templates().map(models::TemplateBody::from).collect();
439        let links = v.policies().map(models::Policy::from).collect();
440        Self { templates, links }
441    }
442}
443
444impl TryFrom<&models::PolicySet> for ast::PolicySet {
445    type Error = ast::ReificationError;
446    fn try_from(pset: &models::PolicySet) -> Result<Self, Self::Error> {
447        ast::PolicySet::try_from(ast::LiteralPolicySet::from(pset))
448    }
449}
450
451#[cfg(test)]
452mod test {
453    use std::sync::Arc;
454
455    use super::*;
456
457    // We add `PartialOrd` and `Ord` implementations for both `models::Policy` and
458    // `models::TemplateBody`, so that these can be sorted for testing purposes
459    impl PartialOrd for models::Policy {
460        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
461            Some(self.cmp(other))
462        }
463    }
464    impl Ord for models::Policy {
465        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
466            // assumes that (link-id, template-id) pair is unique, otherwise we're
467            // technically violating `Ord` contract because there could exist two
468            // policies that return `Ordering::Equal` but are not equal with `Eq`
469            self.link_id()
470                .cmp(other.link_id())
471                .then_with(|| self.template_id.cmp(&other.template_id))
472        }
473    }
474    impl Eq for models::TemplateBody {}
475    impl PartialOrd for models::TemplateBody {
476        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
477            Some(self.cmp(other))
478        }
479    }
480    impl Ord for models::TemplateBody {
481        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
482            // assumes that IDs are unique, otherwise we're technically violating
483            // `Ord` contract because there could exist two template-bodies that
484            // return `Ordering::Equal` but are not equal with `Eq`
485            self.id.cmp(&other.id)
486        }
487    }
488
489    #[test]
490    #[expect(clippy::too_many_lines, reason = "unit test code")]
491    fn policy_roundtrip() {
492        let annotation1 = ast::Annotation {
493            val: "".into(),
494            loc: None,
495        };
496
497        let annotation2 = ast::Annotation {
498            val: "Hello World".into(),
499            loc: None,
500        };
501
502        assert_eq!(
503            ast::Effect::Permit,
504            ast::Effect::from(&models::Effect::from(&ast::Effect::Permit))
505        );
506        assert_eq!(
507            ast::Effect::Forbid,
508            ast::Effect::from(&models::Effect::from(&ast::Effect::Forbid))
509        );
510
511        let er1 = ast::EntityReference::euid(Arc::new(
512            ast::EntityUID::with_eid_and_type("A", "foo").unwrap(),
513        ));
514        assert_eq!(
515            er1,
516            ast::EntityReference::from(&models::EntityReference::from(&er1))
517        );
518        assert_eq!(
519            ast::EntityReference::Slot(None),
520            ast::EntityReference::from(&models::EntityReference::from(
521                &ast::EntityReference::Slot(None)
522            ))
523        );
524
525        let read_euid = Arc::new(ast::EntityUID::with_eid_and_type("Action", "read").unwrap());
526        let write_euid = Arc::new(ast::EntityUID::with_eid_and_type("Action", "write").unwrap());
527        let ac1 = ast::ActionConstraint::Eq(read_euid.clone());
528        let ac2 = ast::ActionConstraint::In(vec![read_euid, write_euid]);
529        assert_eq!(
530            ast::ActionConstraint::Any,
531            ast::ActionConstraint::from(&models::ActionConstraint::from(
532                &ast::ActionConstraint::Any
533            ))
534        );
535        assert_eq!(
536            ac1,
537            ast::ActionConstraint::from(&models::ActionConstraint::from(&ac1))
538        );
539        assert_eq!(
540            ac2,
541            ast::ActionConstraint::from(&models::ActionConstraint::from(&ac2))
542        );
543
544        let euid1 = Arc::new(ast::EntityUID::with_eid_and_type("A", "friend").unwrap());
545        let name1 = Arc::new(ast::EntityType::from(
546            ast::Name::from_normalized_str("B::C::D").unwrap(),
547        ));
548        let prc1 = ast::PrincipalOrResourceConstraint::is_eq(euid1.clone());
549        let prc2 = ast::PrincipalOrResourceConstraint::is_in(euid1.clone());
550        let prc3 = ast::PrincipalOrResourceConstraint::is_entity_type(name1.clone());
551        let prc4 = ast::PrincipalOrResourceConstraint::is_entity_type_in(name1, euid1);
552        assert_eq!(
553            ast::PrincipalOrResourceConstraint::any(),
554            ast::PrincipalOrResourceConstraint::from(&models::PrincipalOrResourceConstraint::from(
555                &ast::PrincipalOrResourceConstraint::any()
556            ))
557        );
558        assert_eq!(
559            prc1,
560            ast::PrincipalOrResourceConstraint::from(&models::PrincipalOrResourceConstraint::from(
561                &prc1
562            ))
563        );
564        assert_eq!(
565            prc2,
566            ast::PrincipalOrResourceConstraint::from(&models::PrincipalOrResourceConstraint::from(
567                &prc2
568            ))
569        );
570        assert_eq!(
571            prc3,
572            ast::PrincipalOrResourceConstraint::from(&models::PrincipalOrResourceConstraint::from(
573                &prc3
574            ))
575        );
576        assert_eq!(
577            prc4,
578            ast::PrincipalOrResourceConstraint::from(&models::PrincipalOrResourceConstraint::from(
579                &prc4
580            ))
581        );
582
583        let pc = ast::PrincipalConstraint::new(prc1);
584        let rc = ast::ResourceConstraint::new(prc3);
585        assert_eq!(
586            pc,
587            ast::PrincipalConstraint::from(&models::PrincipalOrResourceConstraint::from(&pc))
588        );
589        assert_eq!(
590            rc,
591            ast::ResourceConstraint::from(&models::PrincipalOrResourceConstraint::from(&rc))
592        );
593
594        assert_eq!(
595            ast::Effect::Permit,
596            ast::Effect::from(&models::Effect::from(&ast::Effect::Permit))
597        );
598        assert_eq!(
599            ast::Effect::Forbid,
600            ast::Effect::from(&models::Effect::from(&ast::Effect::Forbid))
601        );
602
603        let tb = ast::TemplateBody::new(
604            ast::PolicyID::from_string("template"),
605            None,
606            ast::Annotations::from_iter([
607                (
608                    ast::AnyId::from_normalized_str("read").unwrap(),
609                    annotation1,
610                ),
611                (
612                    ast::AnyId::from_normalized_str("write").unwrap(),
613                    annotation2,
614                ),
615            ]),
616            ast::Effect::Permit,
617            pc.clone(),
618            ac1.clone(),
619            rc.clone(),
620            None,
621        );
622        assert_eq!(
623            tb,
624            ast::TemplateBody::from(&models::TemplateBody::from(&tb))
625        );
626
627        let policy = ast::LiteralPolicy::template_linked_policy(
628            ast::PolicyID::from_string("template"),
629            ast::PolicyID::from_string("id"),
630            HashMap::from_iter([(
631                ast::SlotId::principal(),
632                ast::EntityUID::with_eid_and_type("A", "eid").unwrap(),
633            )]),
634        );
635        assert_eq!(
636            policy,
637            ast::LiteralPolicy::from(&models::Policy::from(&policy))
638        );
639
640        let tb = ast::TemplateBody::new(
641            ast::PolicyID::from_string("\0\n \' \"+-$^!"),
642            None,
643            ast::Annotations::from_iter([]),
644            ast::Effect::Permit,
645            pc,
646            ac1,
647            rc,
648            None,
649        );
650        assert_eq!(
651            tb,
652            ast::TemplateBody::from(&models::TemplateBody::from(&tb))
653        );
654
655        let policy = ast::LiteralPolicy::template_linked_policy(
656            ast::PolicyID::from_string("template\0\n \' \"+-$^!"),
657            ast::PolicyID::from_string("link\0\n \' \"+-$^!"),
658            HashMap::from_iter([(
659                ast::SlotId::principal(),
660                ast::EntityUID::with_eid_and_type("A", "eid").unwrap(),
661            )]),
662        );
663        assert_eq!(
664            policy,
665            ast::LiteralPolicy::from(&models::Policy::from(&policy))
666        );
667    }
668
669    #[test]
670    fn policyset_roundtrip() {
671        let tb = ast::TemplateBody::new(
672            ast::PolicyID::from_string("template"),
673            None,
674            ast::Annotations::from_iter(vec![(
675                ast::AnyId::from_normalized_str("read").unwrap(),
676                ast::Annotation {
677                    val: "".into(),
678                    loc: None,
679                },
680            )]),
681            ast::Effect::Permit,
682            ast::PrincipalConstraint::is_eq_slot(),
683            ast::ActionConstraint::Eq(
684                ast::EntityUID::with_eid_and_type("Action", "read")
685                    .unwrap()
686                    .into(),
687            ),
688            ast::ResourceConstraint::is_entity_type(
689                ast::EntityType::from(ast::Name::from_normalized_str("photo").unwrap()).into(),
690            ),
691            None,
692        );
693
694        let policy1 = ast::Policy::from_when_clause(
695            ast::Effect::Permit,
696            ast::Expr::val(true),
697            ast::PolicyID::from_string("permit-true-trivial"),
698            None,
699        );
700        let policy2 = ast::Policy::from_when_clause(
701            ast::Effect::Forbid,
702            ast::Expr::is_eq(
703                ast::Expr::var(ast::Var::Principal),
704                ast::Expr::val(ast::EntityUID::with_eid_and_type("A", "dog").unwrap()),
705            ),
706            ast::PolicyID::from_string("forbid-dog"),
707            None,
708        );
709
710        let mut ps = ast::PolicySet::new();
711        ps.add_template(ast::Template::from(tb))
712            .expect("Failed to add template to policy set.");
713        ps.add(policy1).expect("Failed to add policy to policy set");
714        ps.add(policy2).expect("Failed to add policy to policy set");
715        ps.link(
716            ast::PolicyID::from_string("template"),
717            ast::PolicyID::from_string("link"),
718            HashMap::from_iter([(
719                ast::SlotId::principal(),
720                ast::EntityUID::with_eid_and_type("A", "friend").unwrap(),
721            )]),
722        )
723        .unwrap();
724        let mut mps = models::PolicySet::from(&ps);
725        let mut mps_roundtrip = models::PolicySet::from(&ast::LiteralPolicySet::from(&mps));
726
727        // we accept permutations as equivalent, so before comparison, we sort
728        // both `.templates` and `.links`
729        mps.templates.sort();
730        mps_roundtrip.templates.sort();
731        mps.links.sort();
732        mps_roundtrip.links.sort();
733
734        // Can't compare `models::PolicySet` directly, so we compare their fields
735        assert_eq!(mps.templates, mps_roundtrip.templates);
736        assert_eq!(mps.links, mps_roundtrip.links);
737    }
738
739    #[test]
740    fn policyset_roundtrip_escapes() {
741        let tb = ast::TemplateBody::new(
742            ast::PolicyID::from_string("template\0\n \' \"+-$^!"),
743            None,
744            ast::Annotations::from_iter(vec![(
745                ast::AnyId::from_normalized_str("read").unwrap(),
746                ast::Annotation {
747                    val: "".into(),
748                    loc: None,
749                },
750            )]),
751            ast::Effect::Permit,
752            ast::PrincipalConstraint::is_eq_slot(),
753            ast::ActionConstraint::Eq(
754                ast::EntityUID::with_eid_and_type("Action", "read")
755                    .unwrap()
756                    .into(),
757            ),
758            ast::ResourceConstraint::is_entity_type(
759                ast::EntityType::from(ast::Name::from_normalized_str("photo").unwrap()).into(),
760            ),
761            None,
762        );
763
764        let policy1 = ast::Policy::from_when_clause(
765            ast::Effect::Permit,
766            ast::Expr::val(true),
767            ast::PolicyID::from_string("permit-true-trivial\0\n \' \"+-$^!"),
768            None,
769        );
770        let policy2 = ast::Policy::from_when_clause(
771            ast::Effect::Forbid,
772            ast::Expr::is_eq(
773                ast::Expr::var(ast::Var::Principal),
774                ast::Expr::val(ast::EntityUID::with_eid_and_type("A", "dog").unwrap()),
775            ),
776            ast::PolicyID::from_string("forbid-dog\0\n \' \"+-$^!"),
777            None,
778        );
779
780        let mut ps = ast::PolicySet::new();
781        ps.add_template(ast::Template::from(tb))
782            .expect("Failed to add template to policy set.");
783        ps.add(policy1).expect("Failed to add policy to policy set");
784        ps.add(policy2).expect("Failed to add policy to policy set");
785        ps.link(
786            ast::PolicyID::from_string("template\0\n \' \"+-$^!"),
787            ast::PolicyID::from_string("link\0\n \' \"+-$^!"),
788            HashMap::from_iter([(
789                ast::SlotId::principal(),
790                ast::EntityUID::with_eid_and_type("A", "friend").unwrap(),
791            )]),
792        )
793        .unwrap();
794        let mut mps = models::PolicySet::from(&ps);
795        let mut mps_roundtrip = models::PolicySet::from(&ast::LiteralPolicySet::from(&mps));
796
797        // we accept permutations as equivalent, so before comparison, we sort
798        // both `.templates` and `.links`
799        mps.templates.sort();
800        mps_roundtrip.templates.sort();
801        mps.links.sort();
802        mps_roundtrip.links.sort();
803
804        // Can't compare `models::PolicySet` directly, so we compare their fields
805        assert_eq!(mps.templates, mps_roundtrip.templates);
806        assert_eq!(mps.links, mps_roundtrip.links);
807    }
808}