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