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