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