1#![allow(clippy::use_self, reason = "readability")]
18
19use super::ast::ProtobufConversionError;
20use super::models;
21use cedar_policy_core::{ast, FromNormalizedStr};
22use linked_hash_map::{Entry, LinkedHashMap};
23use linked_hash_set::LinkedHashSet;
24use std::{
25 collections::{HashMap, HashSet},
26 sync::Arc,
27};
28
29fn reify_template_link(
39 v: models::Policy,
40 link_ids: &mut HashSet<ast::PolicyID>,
41 templates: &LinkedHashMap<ast::PolicyID, Arc<ast::Template>>,
42) -> Result<ast::Policy, ProtobufConversionError> {
43 let template_id = ast::PolicyID::from_string(v.template_id);
44 let template = templates.get(&template_id).ok_or_else(|| {
45 ProtobufConversionError::InvalidValue(format!("no such template: {template_id}"))
46 })?;
47 let link_id = v
48 .link_id
49 .as_ref()
50 .ok_or_else(|| ProtobufConversionError::missing("link_id"))?;
51 let link_id = ast::PolicyID::from_string(link_id);
52 if !link_ids.insert(link_id.clone()) {
53 return Err(ProtobufConversionError::InvalidValue(format!(
54 "link_id `{link_id}` conflicts with a policy id (link_id or template_id) in `links`"
55 )));
56 }
57 if templates.contains_key(&link_id) {
58 return Err(ProtobufConversionError::InvalidValue(format!(
59 "link_id `{link_id}` conflicts with a template_id in `templates`"
60 )));
61 }
62
63 let mut values: ast::SlotEnv = HashMap::new();
64 if let Some(principal_euid) = v.principal_euid {
65 values.insert(
66 ast::SlotId::principal(),
67 ast::EntityUID::try_from(principal_euid)?,
68 );
69 }
70 if let Some(resource_euid) = v.resource_euid {
71 values.insert(
72 ast::SlotId::resource(),
73 ast::EntityUID::try_from(resource_euid)?,
74 );
75 }
76
77 let link_id = ast::PolicyID::from_string(
78 v.link_id
79 .as_ref()
80 .ok_or_else(|| ProtobufConversionError::missing("link_id"))?,
81 );
82 ast::Template::link(template.clone(), link_id, values).map_err(|e| {
83 ProtobufConversionError::InvalidValue(format!("failed to convert to policy: {e}"))
84 })
85}
86
87fn reify_static_policy(
95 v: models::Policy,
96 link_ids: &mut HashSet<ast::PolicyID>,
97 templates: &LinkedHashMap<ast::PolicyID, Arc<ast::Template>>,
98) -> Result<ast::Policy, ProtobufConversionError> {
99 let template_id = ast::PolicyID::from_string(v.template_id);
100 if !link_ids.insert(template_id.clone()) {
102 return Err(ProtobufConversionError::InvalidValue(format!(
103 "template_id `{template_id}` of a static link conflicts with a policy id (link_id or template_id) in `links`"
104 )));
105 }
106 let template = templates.get(&template_id).ok_or_else(|| {
107 ProtobufConversionError::InvalidValue(format!("no such template: {template_id}"))
108 })?;
109 ast::Template::try_as_policy(template.clone()).map_err(|e| {
110 ProtobufConversionError::InvalidValue(format!("failed to convert a static policy: {e}"))
111 })
112}
113
114impl From<&ast::Policy> for models::Policy {
115 fn from(v: &ast::Policy) -> Self {
116 Self {
117 template_id: v.template().id().as_ref().to_string(),
118 link_id: if v.is_static() {
119 None
120 } else {
121 Some(v.id().as_ref().to_string())
122 },
123 is_template_link: !v.is_static(),
124 principal_euid: v
125 .env()
126 .get(&ast::SlotId::principal())
127 .map(models::EntityUid::from),
128 resource_euid: v
129 .env()
130 .get(&ast::SlotId::resource())
131 .map(models::EntityUid::from),
132 }
133 }
134}
135
136impl TryFrom<models::TemplateBody> for ast::Template {
137 type Error = ProtobufConversionError;
138 fn try_from(v: models::TemplateBody) -> Result<Self, Self::Error> {
139 Ok(ast::Template::from(ast::TemplateBody::try_from(v)?))
140 }
141}
142
143impl TryFrom<models::TemplateBody> for ast::TemplateBody {
144 type Error = ProtobufConversionError;
145 fn try_from(v: models::TemplateBody) -> Result<Self, Self::Error> {
146 let effect = models::Effect::try_from(v.effect)
147 .map_err(|e| ProtobufConversionError::InvalidValue(format!("invalid effect: {e}")))?;
148 Ok(ast::TemplateBody::new(
149 ast::PolicyID::from_string(v.id),
150 None,
151 v.annotations
152 .into_iter()
153 .map(|(key, value)| {
154 ast::AnyId::from_normalized_str(&key)
155 .map(|k| {
156 (
157 k,
158 ast::Annotation {
159 val: value.into(),
160 loc: None,
161 },
162 )
163 })
164 .map_err(|e| {
165 ProtobufConversionError::InvalidValue(format!(
166 "invalid annotation key `{key}`: {e}"
167 ))
168 })
169 })
170 .collect::<Result<_, _>>()?,
171 ast::Effect::from(effect),
172 ast::PrincipalConstraint::try_from(
173 v.principal_constraint
174 .ok_or_else(|| ProtobufConversionError::missing("principal_constraint"))?,
175 )?,
176 ast::ActionConstraint::try_from(
177 v.action_constraint
178 .ok_or_else(|| ProtobufConversionError::missing("action_constraint"))?,
179 )?,
180 ast::ResourceConstraint::try_from(
181 v.resource_constraint
182 .ok_or_else(|| ProtobufConversionError::missing("resource_constraint"))?,
183 )?,
184 v.non_scope_constraints
185 .map(ast::Expr::try_from)
186 .transpose()?,
187 ))
188 }
189}
190
191impl From<&ast::TemplateBody> for models::TemplateBody {
192 fn from(v: &ast::TemplateBody) -> Self {
193 let annotations: HashMap<String, String> = v
194 .annotations()
195 .map(|(key, value)| (key.as_ref().into(), value.as_ref().into()))
196 .collect();
197
198 Self {
199 id: v.id().as_ref().to_string(),
200 annotations,
201 effect: models::Effect::from(&v.effect()).into(),
202 principal_constraint: Some(models::PrincipalOrResourceConstraint::from(
203 v.principal_constraint(),
204 )),
205 action_constraint: Some(models::ActionConstraint::from(v.action_constraint())),
206 resource_constraint: Some(models::PrincipalOrResourceConstraint::from(
207 v.resource_constraint(),
208 )),
209 non_scope_constraints: v.non_scope_constraints().map(models::Expr::from),
210 }
211 }
212}
213
214impl From<&ast::Template> for models::TemplateBody {
215 fn from(v: &ast::Template) -> Self {
216 models::TemplateBody::from(&ast::TemplateBody::from(v.clone()))
217 }
218}
219
220impl TryFrom<models::PrincipalOrResourceConstraint> for ast::PrincipalConstraint {
221 type Error = ProtobufConversionError;
222 fn try_from(v: models::PrincipalOrResourceConstraint) -> Result<Self, Self::Error> {
223 Ok(Self::new(ast::PrincipalOrResourceConstraint::try_from(v)?))
224 }
225}
226
227impl From<&ast::PrincipalConstraint> for models::PrincipalOrResourceConstraint {
228 fn from(v: &ast::PrincipalConstraint) -> Self {
229 models::PrincipalOrResourceConstraint::from(v.as_inner())
230 }
231}
232
233impl TryFrom<models::PrincipalOrResourceConstraint> for ast::ResourceConstraint {
234 type Error = ProtobufConversionError;
235 fn try_from(v: models::PrincipalOrResourceConstraint) -> Result<Self, Self::Error> {
236 Ok(Self::new(ast::PrincipalOrResourceConstraint::try_from(v)?))
237 }
238}
239
240impl From<&ast::ResourceConstraint> for models::PrincipalOrResourceConstraint {
241 fn from(v: &ast::ResourceConstraint) -> Self {
242 models::PrincipalOrResourceConstraint::from(v.as_inner())
243 }
244}
245
246impl TryFrom<models::EntityReference> for ast::EntityReference {
247 type Error = ProtobufConversionError;
248 fn try_from(v: models::EntityReference) -> Result<Self, Self::Error> {
249 match v
250 .data
251 .ok_or_else(|| ProtobufConversionError::missing("data"))?
252 {
253 models::entity_reference::Data::Slot(slot) => {
254 match models::entity_reference::Slot::try_from(slot).map_err(|e| {
255 ProtobufConversionError::InvalidValue(format!(
256 "invalid entity reference slot: {e}"
257 ))
258 })? {
259 models::entity_reference::Slot::Unit => Ok(ast::EntityReference::Slot(None)),
260 }
261 }
262 models::entity_reference::Data::Euid(euid) => Ok(ast::EntityReference::euid(
263 ast::EntityUID::try_from(euid)?.into(),
264 )),
265 }
266 }
267}
268
269impl From<&ast::EntityReference> for models::EntityReference {
270 fn from(v: &ast::EntityReference) -> Self {
271 match v {
272 ast::EntityReference::EUID(euid) => Self {
273 data: Some(models::entity_reference::Data::Euid(
274 models::EntityUid::from(euid.as_ref()),
275 )),
276 },
277 ast::EntityReference::Slot(_) => Self {
278 data: Some(models::entity_reference::Data::Slot(
279 models::entity_reference::Slot::Unit.into(),
280 )),
281 },
282 }
283 }
284}
285
286impl TryFrom<models::PrincipalOrResourceConstraint> for ast::PrincipalOrResourceConstraint {
287 type Error = ProtobufConversionError;
288 fn try_from(v: models::PrincipalOrResourceConstraint) -> Result<Self, Self::Error> {
289 match v
290 .data
291 .ok_or_else(|| ProtobufConversionError::missing("data"))?
292 {
293 models::principal_or_resource_constraint::Data::Any(unit) => {
294 match models::principal_or_resource_constraint::Any::try_from(unit).map_err(
295 |e| {
296 ProtobufConversionError::InvalidValue(format!(
297 "invalid principal/resource constraint: {e}"
298 ))
299 },
300 )? {
301 models::principal_or_resource_constraint::Any::Unit => {
302 Ok(ast::PrincipalOrResourceConstraint::Any)
303 }
304 }
305 }
306 models::principal_or_resource_constraint::Data::In(msg) => Ok(
307 ast::PrincipalOrResourceConstraint::In(ast::EntityReference::try_from(
308 msg.er
309 .ok_or_else(|| ProtobufConversionError::missing("er"))?,
310 )?),
311 ),
312 models::principal_or_resource_constraint::Data::Eq(msg) => Ok(
313 ast::PrincipalOrResourceConstraint::Eq(ast::EntityReference::try_from(
314 msg.er
315 .ok_or_else(|| ProtobufConversionError::missing("er"))?,
316 )?),
317 ),
318 models::principal_or_resource_constraint::Data::Is(msg) => {
319 Ok(ast::PrincipalOrResourceConstraint::Is(
320 ast::EntityType::try_from(
321 msg.entity_type
322 .ok_or_else(|| ProtobufConversionError::missing("entity_type"))?,
323 )?
324 .into(),
325 ))
326 }
327 models::principal_or_resource_constraint::Data::IsIn(msg) => {
328 Ok(ast::PrincipalOrResourceConstraint::IsIn(
329 ast::EntityType::try_from(
330 msg.entity_type
331 .ok_or_else(|| ProtobufConversionError::missing("entity_type"))?,
332 )?
333 .into(),
334 ast::EntityReference::try_from(
335 msg.er
336 .ok_or_else(|| ProtobufConversionError::missing("er"))?,
337 )?,
338 ))
339 }
340 }
341 }
342}
343
344impl From<&ast::PrincipalOrResourceConstraint> for models::PrincipalOrResourceConstraint {
345 fn from(v: &ast::PrincipalOrResourceConstraint) -> Self {
346 match v {
347 ast::PrincipalOrResourceConstraint::Any => Self {
348 data: Some(models::principal_or_resource_constraint::Data::Any(
349 models::principal_or_resource_constraint::Any::Unit.into(),
350 )),
351 },
352 ast::PrincipalOrResourceConstraint::In(er) => Self {
353 data: Some(models::principal_or_resource_constraint::Data::In(
354 models::principal_or_resource_constraint::InMessage {
355 er: Some(models::EntityReference::from(er)),
356 },
357 )),
358 },
359 ast::PrincipalOrResourceConstraint::Eq(er) => Self {
360 data: Some(models::principal_or_resource_constraint::Data::Eq(
361 models::principal_or_resource_constraint::EqMessage {
362 er: Some(models::EntityReference::from(er)),
363 },
364 )),
365 },
366 ast::PrincipalOrResourceConstraint::Is(na) => Self {
367 data: Some(models::principal_or_resource_constraint::Data::Is(
368 models::principal_or_resource_constraint::IsMessage {
369 entity_type: Some(models::Name::from(na.as_ref())),
370 },
371 )),
372 },
373 ast::PrincipalOrResourceConstraint::IsIn(na, er) => Self {
374 data: Some(models::principal_or_resource_constraint::Data::IsIn(
375 models::principal_or_resource_constraint::IsInMessage {
376 er: Some(models::EntityReference::from(er)),
377 entity_type: Some(models::Name::from(na.as_ref())),
378 },
379 )),
380 },
381 }
382 }
383}
384
385impl TryFrom<models::ActionConstraint> for ast::ActionConstraint {
386 type Error = ProtobufConversionError;
387 fn try_from(v: models::ActionConstraint) -> Result<Self, Self::Error> {
388 match v
389 .data
390 .ok_or_else(|| ProtobufConversionError::missing("data"))?
391 {
392 models::action_constraint::Data::Any(unit) => {
393 match models::action_constraint::Any::try_from(unit).map_err(|e| {
394 ProtobufConversionError::InvalidValue(format!("invalid action constraint: {e}"))
395 })? {
396 models::action_constraint::Any::Unit => Ok(ast::ActionConstraint::Any),
397 }
398 }
399 models::action_constraint::Data::In(msg) => Ok(ast::ActionConstraint::In(
400 msg.euids
401 .into_iter()
402 .map(|value| Ok(ast::EntityUID::try_from(value)?.into()))
403 .collect::<Result<_, ProtobufConversionError>>()?,
404 )),
405 models::action_constraint::Data::Eq(msg) => Ok(ast::ActionConstraint::Eq(
406 ast::EntityUID::try_from(
407 msg.euid
408 .ok_or_else(|| ProtobufConversionError::missing("euid"))?,
409 )?
410 .into(),
411 )),
412 }
413 }
414}
415
416impl From<&ast::ActionConstraint> for models::ActionConstraint {
417 fn from(v: &ast::ActionConstraint) -> Self {
418 match v {
419 ast::ActionConstraint::Any => Self {
420 data: Some(models::action_constraint::Data::Any(
421 models::action_constraint::Any::Unit.into(),
422 )),
423 },
424 ast::ActionConstraint::In(euids) => {
425 let mut peuids: Vec<models::EntityUid> = Vec::with_capacity(euids.len());
426 for value in euids {
427 peuids.push(models::EntityUid::from(value.as_ref()));
428 }
429 Self {
430 data: Some(models::action_constraint::Data::In(
431 models::action_constraint::InMessage { euids: peuids },
432 )),
433 }
434 }
435 ast::ActionConstraint::Eq(euid) => Self {
436 data: Some(models::action_constraint::Data::Eq(
437 models::action_constraint::EqMessage {
438 euid: Some(models::EntityUid::from(euid.as_ref())),
439 },
440 )),
441 },
442 #[cfg(feature = "tolerant-ast")]
443 #[expect(clippy::unimplemented, reason = "experimental feature")]
444 ast::ActionConstraint::ErrorConstraint => {
445 unimplemented!("tolerant-ast cannot be used with the protobuf feature")
446 }
447 }
448 }
449}
450
451impl From<models::Effect> for ast::Effect {
452 fn from(v: models::Effect) -> Self {
453 match v {
454 models::Effect::Forbid => ast::Effect::Forbid,
455 models::Effect::Permit => ast::Effect::Permit,
456 }
457 }
458}
459
460impl From<&ast::Effect> for models::Effect {
461 fn from(v: &ast::Effect) -> Self {
462 match v {
463 ast::Effect::Permit => models::Effect::Permit,
464 ast::Effect::Forbid => models::Effect::Forbid,
465 }
466 }
467}
468
469impl TryFrom<models::PolicySet> for ast::PolicySet {
470 type Error = ProtobufConversionError;
471 fn try_from(v: models::PolicySet) -> Result<Self, Self::Error> {
472 let mut link_ids: HashSet<ast::PolicyID> = HashSet::new();
473
474 let mut templates: LinkedHashMap<ast::PolicyID, Arc<ast::Template>> = LinkedHashMap::new();
476 for tb in v.templates {
477 let id = ast::PolicyID::from_string(&tb.id);
478 let template = Arc::new(ast::Template::from(ast::TemplateBody::try_from(tb)?));
479 match templates.entry(id.clone()) {
480 Entry::Vacant(entry) => {
481 entry.insert(template);
482 }
483 Entry::Occupied(_) => {
484 return Err(ProtobufConversionError::InvalidValue(format!(
485 "duplicate template_id `{id}` in `templates`"
486 )));
487 }
488 }
489 }
490
491 let mut template_to_links_map: LinkedHashMap<ast::PolicyID, LinkedHashSet<ast::PolicyID>> =
493 LinkedHashMap::new();
494 for template in &templates {
495 template_to_links_map.insert(template.0.clone(), LinkedHashSet::new());
496 }
497 let mut links: LinkedHashMap<ast::PolicyID, ast::Policy> = LinkedHashMap::new();
498 for p in v.links {
499 let template_id = ast::PolicyID::from_string(&p.template_id);
502 if p.is_template_link {
503 let policy = reify_template_link(p, &mut link_ids, &templates)?;
504 template_to_links_map
506 .entry(template_id)
507 .or_insert_with(LinkedHashSet::new)
508 .insert(policy.id().clone());
509 links.insert(policy.id().clone(), policy);
510 } else {
511 let policy = reify_static_policy(p, &mut link_ids, &templates)?;
512 template_to_links_map
514 .entry(template_id.clone())
515 .or_insert_with(LinkedHashSet::new)
516 .insert(template_id.clone());
517 links.insert(template_id, policy);
518 }
519 }
520
521 Ok(ast::PolicySet::from_raw_components(
522 templates,
523 links,
524 template_to_links_map,
525 ))
526 }
527}
528
529impl From<&ast::PolicySet> for models::PolicySet {
530 fn from(v: &ast::PolicySet) -> Self {
531 let templates = v.all_templates().map(models::TemplateBody::from).collect();
532 let links = v.policies().map(models::Policy::from).collect();
533 Self { templates, links }
534 }
535}
536
537#[cfg(test)]
538mod test {
539 use std::sync::Arc;
540
541 use super::*;
542 use cool_asserts::assert_matches;
543
544 impl PartialOrd for models::Policy {
547 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
548 Some(self.cmp(other))
549 }
550 }
551 impl Ord for models::Policy {
552 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
553 self.link_id()
557 .cmp(other.link_id())
558 .then_with(|| self.template_id.cmp(&other.template_id))
559 }
560 }
561 impl Eq for models::TemplateBody {}
562 impl PartialOrd for models::TemplateBody {
563 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
564 Some(self.cmp(other))
565 }
566 }
567 impl Ord for models::TemplateBody {
568 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
569 self.id.cmp(&other.id)
573 }
574 }
575
576 fn principal_or_resource_constraint_any() -> models::PrincipalOrResourceConstraint {
578 models::PrincipalOrResourceConstraint {
579 data: Some(models::principal_or_resource_constraint::Data::Any(
580 models::principal_or_resource_constraint::Any::Unit.into(),
581 )),
582 }
583 }
584
585 fn action_constraint_any() -> models::ActionConstraint {
587 models::ActionConstraint {
588 data: Some(models::action_constraint::Data::Any(
589 models::action_constraint::Any::Unit.into(),
590 )),
591 }
592 }
593
594 fn trivial_template_body(id: &str) -> models::TemplateBody {
596 models::TemplateBody {
597 id: id.to_string(),
598 annotations: Default::default(),
599 effect: models::Effect::Permit.into(),
600 principal_constraint: Some(principal_or_resource_constraint_any()),
601 action_constraint: Some(action_constraint_any()),
602 resource_constraint: Some(principal_or_resource_constraint_any()),
603 non_scope_constraints: None,
604 }
605 }
606
607 fn slotted_template_body(id: &str) -> models::TemplateBody {
609 models::TemplateBody {
610 id: id.to_string(),
611 annotations: Default::default(),
612 effect: models::Effect::Permit.into(),
613 principal_constraint: Some(models::PrincipalOrResourceConstraint {
614 data: Some(models::principal_or_resource_constraint::Data::Eq(
615 models::principal_or_resource_constraint::EqMessage {
616 er: Some(models::EntityReference {
617 data: Some(models::entity_reference::Data::Slot(
618 models::entity_reference::Slot::Unit.into(),
619 )),
620 }),
621 },
622 )),
623 }),
624 action_constraint: Some(action_constraint_any()),
625 resource_constraint: Some(principal_or_resource_constraint_any()),
626 non_scope_constraints: None,
627 }
628 }
629
630 fn static_policy_link(id: &str) -> models::Policy {
632 models::Policy {
633 template_id: id.to_string(),
634 link_id: None,
635 is_template_link: false,
636 principal_euid: None,
637 resource_euid: None,
638 }
639 }
640
641 fn template_link(template_id: &str, link_id: &str) -> models::Policy {
643 models::Policy {
644 template_id: template_id.to_string(),
645 link_id: Some(link_id.to_string()),
646 is_template_link: true,
647 principal_euid: Some(models::EntityUid {
648 ty: Some(models::Name {
649 id: "User".to_string(),
650 path: vec![],
651 }),
652 eid: "alice".to_string(),
653 }),
654 resource_euid: None,
655 }
656 }
657
658 fn slotted_templates_map(id: &str) -> LinkedHashMap<ast::PolicyID, Arc<ast::Template>> {
660 let tb = ast::TemplateBody::new(
661 ast::PolicyID::from_string(id),
662 None,
663 ast::Annotations::from_iter([]),
664 ast::Effect::Permit,
665 ast::PrincipalConstraint::is_eq_slot(),
666 ast::ActionConstraint::Any,
667 ast::ResourceConstraint::any(),
668 None,
669 );
670 LinkedHashMap::from_iter([(
671 ast::PolicyID::from_string(id),
672 Arc::new(ast::Template::from(tb)),
673 )])
674 }
675
676 fn make_linked_policy(
679 template_id: &str,
680 link_id: &str,
681 entity_type: &str,
682 eid: &str,
683 ) -> (
684 ast::Policy,
685 LinkedHashMap<ast::PolicyID, Arc<ast::Template>>,
686 ) {
687 let templates = slotted_templates_map(template_id);
688 let template = templates
689 .get(&ast::PolicyID::from_string(template_id))
690 .unwrap();
691 let policy = ast::Template::link(
692 template.clone(),
693 ast::PolicyID::from_string(link_id),
694 HashMap::from_iter([(
695 ast::SlotId::principal(),
696 ast::EntityUID::with_eid_and_type(entity_type, eid).unwrap(),
697 )]),
698 )
699 .unwrap();
700 (policy, templates)
701 }
702
703 #[test]
704 #[expect(clippy::too_many_lines, reason = "unit test code")]
705 fn policy_roundtrip() {
706 let annotation1 = ast::Annotation {
707 val: "".into(),
708 loc: None,
709 };
710
711 let annotation2 = ast::Annotation {
712 val: "Hello World".into(),
713 loc: None,
714 };
715
716 assert_eq!(
717 ast::Effect::Permit,
718 ast::Effect::from(models::Effect::from(&ast::Effect::Permit))
719 );
720 assert_eq!(
721 ast::Effect::Forbid,
722 ast::Effect::from(models::Effect::from(&ast::Effect::Forbid))
723 );
724
725 let er1 = ast::EntityReference::euid(Arc::new(
726 ast::EntityUID::with_eid_and_type("A", "foo").unwrap(),
727 ));
728 assert_eq!(
729 er1,
730 ast::EntityReference::try_from(models::EntityReference::from(&er1)).unwrap()
731 );
732 assert_eq!(
733 ast::EntityReference::Slot(None),
734 ast::EntityReference::try_from(models::EntityReference::from(
735 &ast::EntityReference::Slot(None)
736 ))
737 .unwrap()
738 );
739
740 let read_euid = Arc::new(ast::EntityUID::with_eid_and_type("Action", "read").unwrap());
741 let write_euid = Arc::new(ast::EntityUID::with_eid_and_type("Action", "write").unwrap());
742 let ac1 = ast::ActionConstraint::Eq(read_euid.clone());
743 let ac2 = ast::ActionConstraint::In(vec![read_euid, write_euid]);
744 assert_eq!(
745 ast::ActionConstraint::Any,
746 ast::ActionConstraint::try_from(models::ActionConstraint::from(
747 &ast::ActionConstraint::Any
748 ))
749 .unwrap()
750 );
751 assert_eq!(
752 ac1,
753 ast::ActionConstraint::try_from(models::ActionConstraint::from(&ac1)).unwrap()
754 );
755 assert_eq!(
756 ac2,
757 ast::ActionConstraint::try_from(models::ActionConstraint::from(&ac2)).unwrap()
758 );
759
760 let euid1 = Arc::new(ast::EntityUID::with_eid_and_type("A", "friend").unwrap());
761 let name1 = Arc::new(ast::EntityType::from(
762 ast::Name::from_normalized_str("B::C::D").unwrap(),
763 ));
764 let prc1 = ast::PrincipalOrResourceConstraint::is_eq(euid1.clone());
765 let prc2 = ast::PrincipalOrResourceConstraint::is_in(euid1.clone());
766 let prc3 = ast::PrincipalOrResourceConstraint::is_entity_type(name1.clone());
767 let prc4 = ast::PrincipalOrResourceConstraint::is_entity_type_in(name1, euid1);
768 assert_eq!(
769 ast::PrincipalOrResourceConstraint::any(),
770 ast::PrincipalOrResourceConstraint::try_from(
771 models::PrincipalOrResourceConstraint::from(
772 &ast::PrincipalOrResourceConstraint::any()
773 )
774 )
775 .unwrap()
776 );
777 assert_eq!(
778 prc1,
779 ast::PrincipalOrResourceConstraint::try_from(
780 models::PrincipalOrResourceConstraint::from(&prc1)
781 )
782 .unwrap()
783 );
784 assert_eq!(
785 prc2,
786 ast::PrincipalOrResourceConstraint::try_from(
787 models::PrincipalOrResourceConstraint::from(&prc2)
788 )
789 .unwrap()
790 );
791 assert_eq!(
792 prc3,
793 ast::PrincipalOrResourceConstraint::try_from(
794 models::PrincipalOrResourceConstraint::from(&prc3)
795 )
796 .unwrap()
797 );
798 assert_eq!(
799 prc4,
800 ast::PrincipalOrResourceConstraint::try_from(
801 models::PrincipalOrResourceConstraint::from(&prc4)
802 )
803 .unwrap()
804 );
805
806 let pc = ast::PrincipalConstraint::new(prc1);
807 let rc = ast::ResourceConstraint::new(prc3);
808 assert_eq!(
809 pc,
810 ast::PrincipalConstraint::try_from(models::PrincipalOrResourceConstraint::from(&pc))
811 .unwrap()
812 );
813 assert_eq!(
814 rc,
815 ast::ResourceConstraint::try_from(models::PrincipalOrResourceConstraint::from(&rc))
816 .unwrap()
817 );
818
819 assert_eq!(
820 ast::Effect::Permit,
821 ast::Effect::from(models::Effect::from(&ast::Effect::Permit))
822 );
823 assert_eq!(
824 ast::Effect::Forbid,
825 ast::Effect::from(models::Effect::from(&ast::Effect::Forbid))
826 );
827
828 let tb = ast::TemplateBody::new(
829 ast::PolicyID::from_string("template"),
830 None,
831 ast::Annotations::from_iter([
832 (
833 ast::AnyId::from_normalized_str("read").unwrap(),
834 annotation1,
835 ),
836 (
837 ast::AnyId::from_normalized_str("write").unwrap(),
838 annotation2,
839 ),
840 ]),
841 ast::Effect::Permit,
842 pc.clone(),
843 ac1.clone(),
844 rc.clone(),
845 None,
846 );
847 assert_eq!(
848 tb,
849 ast::TemplateBody::try_from(models::TemplateBody::from(&tb)).unwrap()
850 );
851
852 let (linked_policy, templates) = make_linked_policy("template", "id", "A", "eid");
854 let model = models::Policy::from(&linked_policy);
855 let mut link_ids = HashSet::new();
856 let roundtripped = reify_template_link(model, &mut link_ids, &templates).unwrap();
857 assert_eq!(linked_policy, roundtripped);
858
859 let tb = ast::TemplateBody::new(
860 ast::PolicyID::from_string("\0\n \' \"+-$^!"),
861 None,
862 ast::Annotations::from_iter([]),
863 ast::Effect::Permit,
864 pc,
865 ac1,
866 rc,
867 None,
868 );
869 assert_eq!(
870 tb,
871 ast::TemplateBody::try_from(models::TemplateBody::from(&tb)).unwrap()
872 );
873
874 let (linked_policy2, templates2) =
876 make_linked_policy("template\0\n \' \"+-$^!", "link\0\n \' \"+-$^!", "A", "eid");
877 let model2 = models::Policy::from(&linked_policy2);
878 let mut link_ids = HashSet::new();
879 let roundtripped2 = reify_template_link(model2, &mut link_ids, &templates2).unwrap();
880 assert_eq!(linked_policy2, roundtripped2);
881 }
882
883 #[test]
884 fn policyset_roundtrip() {
885 let tb = ast::TemplateBody::new(
886 ast::PolicyID::from_string("template"),
887 None,
888 ast::Annotations::from_iter(vec![(
889 ast::AnyId::from_normalized_str("read").unwrap(),
890 ast::Annotation {
891 val: "".into(),
892 loc: None,
893 },
894 )]),
895 ast::Effect::Permit,
896 ast::PrincipalConstraint::is_eq_slot(),
897 ast::ActionConstraint::Eq(
898 ast::EntityUID::with_eid_and_type("Action", "read")
899 .unwrap()
900 .into(),
901 ),
902 ast::ResourceConstraint::is_entity_type(
903 ast::EntityType::from(ast::Name::from_normalized_str("photo").unwrap()).into(),
904 ),
905 None,
906 );
907
908 let policy1 = ast::Policy::from_when_clause(
909 ast::Effect::Permit,
910 ast::Expr::val(true),
911 ast::PolicyID::from_string("permit-true-trivial"),
912 None,
913 );
914 let policy2 = ast::Policy::from_when_clause(
915 ast::Effect::Forbid,
916 ast::Expr::is_eq(
917 ast::Expr::var(ast::Var::Principal),
918 ast::Expr::val(ast::EntityUID::with_eid_and_type("A", "dog").unwrap()),
919 ),
920 ast::PolicyID::from_string("forbid-dog"),
921 None,
922 );
923
924 let mut ps = ast::PolicySet::new();
925 ps.add_template(ast::Template::from(tb))
926 .expect("Failed to add template to policy set.");
927 ps.add(policy1).expect("Failed to add policy to policy set");
928 ps.add(policy2).expect("Failed to add policy to policy set");
929 ps.link(
930 ast::PolicyID::from_string("template"),
931 ast::PolicyID::from_string("link"),
932 HashMap::from_iter([(
933 ast::SlotId::principal(),
934 ast::EntityUID::with_eid_and_type("A", "friend").unwrap(),
935 )]),
936 )
937 .unwrap();
938 let mut mps = models::PolicySet::from(&ps);
939 let mut mps_roundtrip =
940 models::PolicySet::from(&ast::PolicySet::try_from(mps.clone()).unwrap());
941
942 mps.templates.sort();
945 mps_roundtrip.templates.sort();
946 mps.links.sort();
947 mps_roundtrip.links.sort();
948
949 assert_eq!(mps.templates, mps_roundtrip.templates);
951 assert_eq!(mps.links, mps_roundtrip.links);
952 }
953
954 #[test]
955 fn policyset_roundtrip_escapes() {
956 let tb = ast::TemplateBody::new(
957 ast::PolicyID::from_string("template\0\n \' \"+-$^!"),
958 None,
959 ast::Annotations::from_iter(vec![(
960 ast::AnyId::from_normalized_str("read").unwrap(),
961 ast::Annotation {
962 val: "".into(),
963 loc: None,
964 },
965 )]),
966 ast::Effect::Permit,
967 ast::PrincipalConstraint::is_eq_slot(),
968 ast::ActionConstraint::Eq(
969 ast::EntityUID::with_eid_and_type("Action", "read")
970 .unwrap()
971 .into(),
972 ),
973 ast::ResourceConstraint::is_entity_type(
974 ast::EntityType::from(ast::Name::from_normalized_str("photo").unwrap()).into(),
975 ),
976 None,
977 );
978
979 let policy1 = ast::Policy::from_when_clause(
980 ast::Effect::Permit,
981 ast::Expr::val(true),
982 ast::PolicyID::from_string("permit-true-trivial\0\n \' \"+-$^!"),
983 None,
984 );
985 let policy2 = ast::Policy::from_when_clause(
986 ast::Effect::Forbid,
987 ast::Expr::is_eq(
988 ast::Expr::var(ast::Var::Principal),
989 ast::Expr::val(ast::EntityUID::with_eid_and_type("A", "dog").unwrap()),
990 ),
991 ast::PolicyID::from_string("forbid-dog\0\n \' \"+-$^!"),
992 None,
993 );
994
995 let mut ps = ast::PolicySet::new();
996 ps.add_template(ast::Template::from(tb))
997 .expect("Failed to add template to policy set.");
998 ps.add(policy1).expect("Failed to add policy to policy set");
999 ps.add(policy2).expect("Failed to add policy to policy set");
1000 ps.link(
1001 ast::PolicyID::from_string("template\0\n \' \"+-$^!"),
1002 ast::PolicyID::from_string("link\0\n \' \"+-$^!"),
1003 HashMap::from_iter([(
1004 ast::SlotId::principal(),
1005 ast::EntityUID::with_eid_and_type("A", "friend").unwrap(),
1006 )]),
1007 )
1008 .unwrap();
1009 let mut mps = models::PolicySet::from(&ps);
1010 let mut mps_roundtrip =
1011 models::PolicySet::from(&ast::PolicySet::try_from(mps.clone()).unwrap());
1012
1013 mps.templates.sort();
1016 mps_roundtrip.templates.sort();
1017 mps.links.sort();
1018 mps_roundtrip.links.sort();
1019
1020 assert_eq!(mps.templates, mps_roundtrip.templates);
1022 assert_eq!(mps.links, mps_roundtrip.links);
1023 }
1024
1025 #[test]
1026 fn template_body_try_from_missing_principal_constraint() {
1027 let bad = models::TemplateBody {
1028 id: "t".to_string(),
1029 annotations: Default::default(),
1030 effect: models::Effect::Permit.into(),
1031 principal_constraint: None,
1032 action_constraint: Some(action_constraint_any()),
1033 resource_constraint: Some(principal_or_resource_constraint_any()),
1034 non_scope_constraints: None,
1035 };
1036 assert_matches!(
1037 ast::TemplateBody::try_from(bad),
1038 Err(ProtobufConversionError::MissingField(f)) if f == "principal_constraint"
1039 );
1040 }
1041
1042 #[test]
1043 fn template_body_try_from_invalid_annotation_key() {
1044 let bad = models::TemplateBody {
1045 id: "t".to_string(),
1046 annotations: [("".to_string(), "v".to_string())].into_iter().collect(),
1047 effect: models::Effect::Permit.into(),
1048 principal_constraint: Some(principal_or_resource_constraint_any()),
1049 action_constraint: Some(action_constraint_any()),
1050 resource_constraint: Some(principal_or_resource_constraint_any()),
1051 non_scope_constraints: None,
1052 };
1053 assert_matches!(
1054 ast::TemplateBody::try_from(bad),
1055 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("invalid annotation key")
1056 );
1057 }
1058
1059 #[test]
1060 fn entity_reference_try_from_missing_data() {
1061 let bad = models::EntityReference { data: None };
1062 assert_matches!(
1063 ast::EntityReference::try_from(bad),
1064 Err(ProtobufConversionError::MissingField(f)) if f == "data"
1065 );
1066 }
1067
1068 #[test]
1069 fn principal_or_resource_constraint_try_from_missing_data() {
1070 let bad = models::PrincipalOrResourceConstraint { data: None };
1071 assert_matches!(
1072 ast::PrincipalOrResourceConstraint::try_from(bad),
1073 Err(ProtobufConversionError::MissingField(f)) if f == "data"
1074 );
1075 }
1076
1077 #[test]
1078 fn principal_or_resource_constraint_try_from_in_missing_er() {
1079 let bad = models::PrincipalOrResourceConstraint {
1080 data: Some(models::principal_or_resource_constraint::Data::In(
1081 models::principal_or_resource_constraint::InMessage { er: None },
1082 )),
1083 };
1084 assert_matches!(
1085 ast::PrincipalOrResourceConstraint::try_from(bad),
1086 Err(ProtobufConversionError::MissingField(f)) if f == "er"
1087 );
1088 }
1089
1090 #[test]
1091 fn principal_or_resource_constraint_try_from_eq_missing_er() {
1092 let bad = models::PrincipalOrResourceConstraint {
1093 data: Some(models::principal_or_resource_constraint::Data::Eq(
1094 models::principal_or_resource_constraint::EqMessage { er: None },
1095 )),
1096 };
1097 assert_matches!(
1098 ast::PrincipalOrResourceConstraint::try_from(bad),
1099 Err(ProtobufConversionError::MissingField(f)) if f == "er"
1100 );
1101 }
1102
1103 #[test]
1104 fn principal_or_resource_constraint_try_from_is_missing_entity_type() {
1105 let bad = models::PrincipalOrResourceConstraint {
1106 data: Some(models::principal_or_resource_constraint::Data::Is(
1107 models::principal_or_resource_constraint::IsMessage { entity_type: None },
1108 )),
1109 };
1110 assert_matches!(
1111 ast::PrincipalOrResourceConstraint::try_from(bad),
1112 Err(ProtobufConversionError::MissingField(f)) if f == "entity_type"
1113 );
1114 }
1115
1116 #[test]
1117 fn principal_or_resource_constraint_try_from_is_in_missing_entity_type() {
1118 let bad = models::PrincipalOrResourceConstraint {
1119 data: Some(models::principal_or_resource_constraint::Data::IsIn(
1120 models::principal_or_resource_constraint::IsInMessage {
1121 entity_type: None,
1122 er: None,
1123 },
1124 )),
1125 };
1126 assert_matches!(
1127 ast::PrincipalOrResourceConstraint::try_from(bad),
1128 Err(ProtobufConversionError::MissingField(f)) if f == "entity_type"
1129 );
1130 }
1131
1132 #[test]
1133 fn action_constraint_try_from_missing_data() {
1134 let bad = models::ActionConstraint { data: None };
1135 assert_matches!(
1136 ast::ActionConstraint::try_from(bad),
1137 Err(ProtobufConversionError::MissingField(f)) if f == "data"
1138 );
1139 }
1140
1141 #[test]
1142 fn action_constraint_try_from_eq_missing_euid() {
1143 let bad = models::ActionConstraint {
1144 data: Some(models::action_constraint::Data::Eq(
1145 models::action_constraint::EqMessage { euid: None },
1146 )),
1147 };
1148 assert_matches!(
1149 ast::ActionConstraint::try_from(bad),
1150 Err(ProtobufConversionError::MissingField(f)) if f == "euid"
1151 );
1152 }
1153
1154 #[test]
1155 fn reify_rejects_template_link_missing_link_id() {
1156 let bad = models::Policy {
1157 template_id: "t".to_string(),
1158 link_id: None,
1159 is_template_link: true,
1160 principal_euid: None,
1161 resource_euid: None,
1162 };
1163 let templates = LinkedHashMap::from_iter([(
1164 ast::PolicyID::from_string("t"),
1165 Arc::new(ast::Template::from(
1166 ast::TemplateBody::try_from(trivial_template_body("t")).unwrap(),
1167 )),
1168 )]);
1169 let mut link_ids = HashSet::new();
1170 assert_matches!(
1171 reify_template_link(bad, &mut link_ids, &templates),
1172 Err(ProtobufConversionError::MissingField(f)) if f == "link_id"
1173 );
1174 }
1175
1176 #[test]
1177 fn reify_rejects_link_with_wrong_slots() {
1178 let templates = slotted_templates_map("t");
1180 let bad = models::Policy {
1181 template_id: "t".to_string(),
1182 link_id: Some("link".to_string()),
1183 is_template_link: true,
1184 principal_euid: None,
1185 resource_euid: Some(models::EntityUid {
1186 ty: Some(models::Name {
1187 id: "User".to_string(),
1188 path: vec![],
1189 }),
1190 eid: "alice".to_string(),
1191 }),
1192 };
1193 let mut link_ids = HashSet::new();
1194 assert_matches!(
1195 reify_template_link(bad, &mut link_ids, &templates),
1196 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("failed to convert to policy")
1197 );
1198 }
1199
1200 #[test]
1201 fn reify_rejects_static_policy_from_slotted_template() {
1202 let templates = slotted_templates_map("t");
1204 let bad = models::Policy {
1205 template_id: "t".to_string(),
1206 link_id: None,
1207 is_template_link: false,
1208 principal_euid: None,
1209 resource_euid: None,
1210 };
1211 let mut link_ids = HashSet::new();
1212 assert_matches!(
1213 reify_static_policy(bad, &mut link_ids, &templates),
1214 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("failed to convert a static policy")
1215 );
1216 }
1217
1218 #[test]
1219 fn template_body_try_from_missing_action_constraint() {
1220 let bad = models::TemplateBody {
1221 action_constraint: None,
1222 ..trivial_template_body("t")
1223 };
1224 assert_matches!(
1225 ast::TemplateBody::try_from(bad),
1226 Err(ProtobufConversionError::MissingField(f)) if f == "action_constraint"
1227 );
1228 }
1229
1230 #[test]
1231 fn template_body_try_from_missing_resource_constraint() {
1232 let bad = models::TemplateBody {
1233 resource_constraint: None,
1234 ..trivial_template_body("t")
1235 };
1236 assert_matches!(
1237 ast::TemplateBody::try_from(bad),
1238 Err(ProtobufConversionError::MissingField(f)) if f == "resource_constraint"
1239 );
1240 }
1241
1242 #[test]
1243 fn action_constraint_try_from_in_with_invalid_euid() {
1244 let bad = models::ActionConstraint {
1245 data: Some(models::action_constraint::Data::In(
1246 models::action_constraint::InMessage {
1247 euids: vec![models::EntityUid {
1248 ty: None,
1249 eid: "read".to_string(),
1250 }],
1251 },
1252 )),
1253 };
1254 assert_matches!(
1255 ast::ActionConstraint::try_from(bad),
1256 Err(ProtobufConversionError::MissingField(f)) if f == "ty"
1257 );
1258 }
1259
1260 #[test]
1261 fn literal_policy_set_try_from_link_missing_link_id() {
1262 let bad = models::PolicySet {
1263 templates: vec![trivial_template_body("t")],
1264 links: vec![models::Policy {
1265 template_id: "t".to_string(),
1266 link_id: None,
1267 is_template_link: true,
1268 principal_euid: None,
1269 resource_euid: None,
1270 }],
1271 };
1272 assert_matches!(
1273 ast::PolicySet::try_from(bad),
1274 Err(ProtobufConversionError::MissingField(f)) if f == "link_id"
1275 );
1276 }
1277
1278 #[test]
1279 fn literal_policy_set_rejects_duplicate_template_ids() {
1280 let template = trivial_template_body("duplicate");
1281 let bad = models::PolicySet {
1282 templates: vec![template.clone(), template], links: vec![],
1284 };
1285 assert_matches!(
1286 ast::PolicySet::try_from(bad),
1287 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("duplicate template_id")
1288 );
1289 }
1290
1291 #[test]
1292 fn literal_policy_set_rejects_duplicate_link_ids() {
1293 let bad = models::PolicySet {
1294 templates: vec![trivial_template_body("duplicate")],
1295 links: vec![
1296 static_policy_link("duplicate"),
1297 static_policy_link("duplicate"),
1298 ],
1299 };
1300 assert_matches!(
1301 ast::PolicySet::try_from(bad),
1302 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("conflicts with a policy id")
1303 );
1304 }
1305
1306 #[test]
1307 fn literal_policy_set_rejects_template_link_id_colliding_with_template_id() {
1308 let bad = models::PolicySet {
1309 templates: vec![trivial_template_body("shared_id")],
1310 links: vec![template_link("shared_id", "shared_id")],
1311 };
1312 assert_matches!(
1313 ast::PolicySet::try_from(bad),
1314 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("conflicts with a template_id")
1315 );
1316 }
1317
1318 #[test]
1319 fn literal_policy_set_allows_static_policy_sharing_template_and_link_id() {
1320 let pset = models::PolicySet {
1322 templates: vec![trivial_template_body("static_policy")],
1323 links: vec![static_policy_link("static_policy")],
1324 };
1325 assert_matches!(ast::PolicySet::try_from(pset), Ok(_));
1328 }
1329
1330 #[test]
1331 fn literal_policy_set_rejects_link_referencing_nonexistent_template() {
1332 let bad = models::PolicySet {
1333 templates: vec![trivial_template_body("real_template")],
1334 links: vec![static_policy_link("nonexistent")],
1335 };
1336 assert_matches!(
1337 ast::PolicySet::try_from(bad),
1338 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("no such template")
1339 );
1340 }
1341
1342 #[test]
1343 fn literal_policy_set_rejects_duplicate_template_link_ids() {
1344 let bad = models::PolicySet {
1345 templates: vec![slotted_template_body("t")],
1346 links: vec![
1347 template_link("t", "same_link"),
1348 template_link("t", "same_link"),
1349 ],
1350 };
1351 assert_matches!(
1352 ast::PolicySet::try_from(bad),
1353 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("conflicts with a policy id")
1354 );
1355 }
1356
1357 #[test]
1358 fn literal_policy_set_rejects_template_link_id_colliding_with_static_policy() {
1359 let bad = models::PolicySet {
1362 templates: vec![trivial_template_body("X"), trivial_template_body("T")],
1363 links: vec![static_policy_link("X"), template_link("T", "X")],
1364 };
1365 assert_matches!(
1366 ast::PolicySet::try_from(bad),
1367 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("conflicts with a policy id")
1368 );
1369 }
1370
1371 #[test]
1372 fn literal_policy_set_rejects_static_policy_colliding_with_template_link_id() {
1373 let bad = models::PolicySet {
1377 templates: vec![slotted_template_body("T"), trivial_template_body("X")],
1378 links: vec![template_link("T", "X"), static_policy_link("X")],
1379 };
1380 assert_matches!(
1381 ast::PolicySet::try_from(bad),
1382 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("conflicts with a template_id")
1383 );
1384 }
1385
1386 #[test]
1390 fn noisy_policy_set_rejects_duplicate_template_ids() {
1391 let bad = models::PolicySet {
1392 templates: vec![
1393 trivial_template_body("ok1"),
1394 slotted_template_body("ok2"),
1395 trivial_template_body("duplicate"),
1396 trivial_template_body("duplicate"),
1397 ],
1398 links: vec![static_policy_link("ok1"), template_link("ok2", "link1")],
1399 };
1400 assert_matches!(
1401 ast::PolicySet::try_from(bad),
1402 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("duplicate template_id")
1403 );
1404 }
1405
1406 #[test]
1407 fn noisy_policy_set_rejects_duplicate_link_ids() {
1408 let bad = models::PolicySet {
1409 templates: vec![
1410 trivial_template_body("t1"),
1411 trivial_template_body("t2"),
1412 slotted_template_body("t3"),
1413 ],
1414 links: vec![
1415 static_policy_link("t1"),
1416 template_link("t3", "link_ok"),
1417 static_policy_link("t2"),
1418 static_policy_link("t2"), ],
1420 };
1421 assert_matches!(
1422 ast::PolicySet::try_from(bad),
1423 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("conflicts with a policy id")
1424 );
1425 }
1426
1427 #[test]
1428 fn noisy_policy_set_rejects_template_link_id_colliding_with_template_id() {
1429 let bad = models::PolicySet {
1430 templates: vec![
1431 trivial_template_body("t1"),
1432 slotted_template_body("t2"),
1433 trivial_template_body("collide"),
1434 ],
1435 links: vec![
1436 static_policy_link("t1"),
1437 template_link("t2", "link_ok"),
1438 template_link("t2", "collide"), ],
1440 };
1441 assert_matches!(
1442 ast::PolicySet::try_from(bad),
1443 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("conflicts with a template_id")
1444 );
1445 }
1446
1447 #[test]
1448 fn noisy_policy_set_rejects_nonexistent_template() {
1449 let bad = models::PolicySet {
1450 templates: vec![trivial_template_body("t1"), slotted_template_body("t2")],
1451 links: vec![
1452 static_policy_link("t1"),
1453 template_link("t2", "link_ok"),
1454 static_policy_link("ghost"), ],
1456 };
1457 assert_matches!(
1458 ast::PolicySet::try_from(bad),
1459 Err(ProtobufConversionError::InvalidValue(msg)) if msg.contains("no such template")
1460 );
1461 }
1462
1463 #[test]
1464 fn noisy_policy_set_valid_with_multiple_templates_and_links() {
1465 let pset = models::PolicySet {
1466 templates: vec![
1467 trivial_template_body("static1"),
1468 trivial_template_body("static2"),
1469 slotted_template_body("tmpl1"),
1470 slotted_template_body("tmpl2"),
1471 ],
1472 links: vec![
1473 static_policy_link("static1"),
1474 static_policy_link("static2"),
1475 template_link("tmpl1", "link1"),
1476 template_link("tmpl1", "link2"),
1477 template_link("tmpl2", "link3"),
1478 ],
1479 };
1480 let ps = ast::PolicySet::try_from(pset).unwrap();
1481 assert_eq!(ps.all_templates().count(), 4);
1482 assert_eq!(ps.policies().count(), 5);
1483 }
1484}