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