1#![allow(clippy::module_name_repetitions)]
20use super::utils::{InterfaceResult, PolicySpecification};
21use crate::api::EntityId;
22use crate::api::EntityTypeName;
23use crate::PolicyId;
24use crate::{
25 Authorizer, Context, Decision, Entities, EntityUid, ParseErrors, Policy, PolicySet, Request,
26 Response, Schema, SlotId, Template,
27};
28use cedar_policy_core::jsonvalue::JsonValueWithNoDuplicateKeys;
29use itertools::Itertools;
30use serde::{Deserialize, Serialize};
31use serde_with::serde_as;
32use serde_with::MapPreventDuplicates;
33use std::collections::{HashMap, HashSet};
34use std::str::FromStr;
35use thiserror::Error;
36
37thread_local!(
38 static AUTHORIZER: Authorizer = Authorizer::new();
40);
41
42fn is_authorized(call: AuthorizationCall) -> AuthorizationAnswer {
44 match call.get_components() {
45 Ok((request, policies, entities)) => {
46 AUTHORIZER.with(|authorizer| AuthorizationAnswer::Success {
47 response: authorizer
48 .is_authorized(&request, &policies, &entities)
49 .into(),
50 })
51 }
52 Err(errors) => AuthorizationAnswer::ParseFailed { errors },
53 }
54}
55
56pub fn json_is_authorized(input: &str) -> InterfaceResult {
61 serde_json::from_str::<AuthorizationCall>(input).map_or_else(
62 |e| InterfaceResult::fail_internally(format!("error parsing call: {e:}")),
63 |call| match is_authorized(call) {
64 answer @ AuthorizationAnswer::Success { .. } => InterfaceResult::succeed(answer),
65 AuthorizationAnswer::ParseFailed { errors } => {
66 InterfaceResult::fail_bad_request(errors)
67 }
68 },
69 )
70}
71
72#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
74pub struct InterfaceResponse {
75 decision: Decision,
77 diagnostics: InterfaceDiagnostics,
79}
80
81#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
83pub struct InterfaceDiagnostics {
84 reason: HashSet<PolicyId>,
87 errors: HashSet<String>,
89}
90
91impl InterfaceResponse {
92 pub fn new(decision: Decision, reason: HashSet<PolicyId>, errors: HashSet<String>) -> Self {
94 Self {
95 decision,
96 diagnostics: InterfaceDiagnostics { reason, errors },
97 }
98 }
99
100 pub fn decision(&self) -> Decision {
102 self.decision
103 }
104
105 pub fn diagnostics(&self) -> &InterfaceDiagnostics {
107 &self.diagnostics
108 }
109}
110
111impl From<Response> for InterfaceResponse {
112 fn from(response: Response) -> Self {
113 Self::new(
114 response.decision(),
115 response.diagnostics().reason().cloned().collect(),
116 response
117 .diagnostics()
118 .errors()
119 .map(ToString::to_string)
120 .collect(),
121 )
122 }
123}
124
125impl InterfaceDiagnostics {
126 pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
128 self.reason.iter()
129 }
130
131 pub fn errors(&self) -> impl Iterator<Item = &str> + '_ {
133 self.errors.iter().map(String::as_str)
134 }
135}
136
137#[derive(Debug, Serialize, Deserialize)]
138#[serde(untagged)]
139enum AuthorizationAnswer {
140 ParseFailed { errors: Vec<String> },
141 Success { response: InterfaceResponse },
142}
143
144#[serde_as]
145#[derive(Debug, Serialize, Deserialize)]
146struct AuthorizationCall {
147 principal: Option<JsonValueWithNoDuplicateKeys>,
148 action: JsonValueWithNoDuplicateKeys,
149 resource: Option<JsonValueWithNoDuplicateKeys>,
150 #[serde_as(as = "MapPreventDuplicates<_, _>")]
151 context: HashMap<String, JsonValueWithNoDuplicateKeys>,
152 #[serde(rename = "schema")]
157 schema: Option<JsonValueWithNoDuplicateKeys>,
158 #[serde(default = "constant_true")]
163 enable_request_validation: bool,
164 slice: RecvdSlice,
165}
166
167fn constant_true() -> bool {
168 true
169}
170
171impl AuthorizationCall {
172 fn get_components(self) -> Result<(Request, PolicySet, Entities), Vec<String>> {
173 let schema = self
174 .schema
175 .map(|v| Schema::from_json_value(v.into()))
176 .transpose()
177 .map_err(|e| [e.to_string()])?;
178 let principal = match self.principal {
179 Some(p) => Some(
180 EntityUid::from_json(p.into())
181 .map_err(|e| ["Failed to parse principal".into(), e.to_string()])?,
182 ),
183 None => None,
184 };
185 let action = EntityUid::from_json(self.action.into())
186 .map_err(|e| ["Failed to parse action".into(), e.to_string()])?;
187 let resource = match self.resource {
188 Some(r) => Some(
189 EntityUid::from_json(r.into())
190 .map_err(|e| ["Failed to parse resource".into(), e.to_string()])?,
191 ),
192 None => None,
193 };
194
195 let context = serde_json::to_value(self.context)
196 .map_err(|e| [format!("Error encoding the context as JSON: {e}")])?;
197 let context = Context::from_json_value(context, schema.as_ref().map(|s| (s, &action)))
198 .map_err(|e| [e.to_string()])?;
199 let q = Request::new(
200 principal,
201 Some(action),
202 resource,
203 context,
204 if self.enable_request_validation {
205 schema.as_ref()
206 } else {
207 None
208 },
209 )
210 .map_err(|e| [e.to_string()])?;
211 let (policies, entities) = self.slice.try_into(schema.as_ref())?;
212 Ok((q, policies, entities))
213 }
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
220struct EntityUIDStrings {
221 ty: String,
222 eid: String,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
226struct Link {
227 slot: String,
228 value: EntityUIDStrings,
229}
230
231#[derive(Debug, Serialize, Deserialize)]
232struct TemplateLink {
233 template_id: String,
235
236 result_policy_id: String,
238
239 instantiations: Links,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
245#[serde(try_from = "Vec<Link>")]
246#[serde(into = "Vec<Link>")]
247struct Links(Vec<Link>);
248
249#[derive(Debug, Clone, Error)]
251pub enum DuplicateLinkError {
252 #[error("duplicate instantiations of the slot(s): {}", .0.iter().map(|s| format!("`{s}`")).join(", "))]
254 Duplicates(Vec<String>),
255}
256
257impl TryFrom<Vec<Link>> for Links {
258 type Error = DuplicateLinkError;
259
260 fn try_from(links: Vec<Link>) -> Result<Self, Self::Error> {
261 let mut slots = links.iter().map(|link| &link.slot).collect::<Vec<_>>();
262 slots.sort();
263 let duplicates = slots
264 .into_iter()
265 .dedup_with_count()
266 .filter_map(|(count, slot)| if count == 1 { None } else { Some(slot) })
267 .cloned()
268 .collect::<Vec<_>>();
269 if duplicates.is_empty() {
270 Ok(Self(links))
271 } else {
272 Err(DuplicateLinkError::Duplicates(duplicates))
273 }
274 }
275}
276
277impl From<Links> for Vec<Link> {
278 fn from(value: Links) -> Self {
279 value.0
280 }
281}
282
283#[serde_as]
285#[derive(Debug, Serialize, Deserialize)]
286struct RecvdSlice {
287 policies: PolicySpecification,
288 entities: JsonValueWithNoDuplicateKeys,
291
292 #[serde_as(as = "Option<MapPreventDuplicates<_, _>>")]
294 templates: Option<HashMap<String, String>>,
295
296 template_instantiations: Option<Vec<TemplateLink>>,
300}
301
302fn parse_instantiation(v: &Link) -> Result<(SlotId, EntityUid), Vec<String>> {
303 let slot = match v.slot.as_str() {
304 "?principal" => SlotId::principal(),
305 "?resource" => SlotId::resource(),
306 _ => {
307 return Err(vec![
308 "Slot must by \"?principal\" or \"?resource\"".to_string()
309 ]);
310 }
311 };
312 let type_name = EntityTypeName::from_str(v.value.ty.as_str());
313 let eid = match EntityId::from_str(v.value.eid.as_str()) {
314 Ok(eid) => eid,
315 Err(err) => match err {},
316 };
317 match type_name {
318 Ok(type_name) => {
319 let entity_uid = EntityUid::from_type_name_and_id(type_name, eid);
320 Ok((slot, entity_uid))
321 }
322 Err(e) => Err(e.errors_as_strings()),
323 }
324}
325
326fn parse_instantiations(
327 policies: &mut PolicySet,
328 instantiation: TemplateLink,
329) -> Result<(), Vec<String>> {
330 let template_id = PolicyId::from_str(instantiation.template_id.as_str());
331 let instance_id = PolicyId::from_str(instantiation.result_policy_id.as_str());
332 match (template_id, instance_id) {
333 (Ok(_), Err(e)) | (Err(e), Ok(_)) => Err(e.errors_as_strings()),
334 (Err(mut e1), Err(mut e2)) => {
335 e1.0.append(&mut e2.0);
336 Err(ParseErrors(e1.0).errors_as_strings())
337 }
338 (Ok(template_id), Ok(instance_id)) => {
339 let mut vals = HashMap::new();
340 for i in instantiation.instantiations.0 {
341 match parse_instantiation(&i) {
342 Err(e) => return Err(e),
343 Ok(val) => vals.insert(val.0, val.1),
344 };
345 }
346 match policies.link(template_id, instance_id, vals) {
347 Ok(()) => Ok(()),
348 Err(e) => Err(vec![format!("Error instantiating template: {e}")]),
349 }
350 }
351 }
352}
353
354impl RecvdSlice {
355 #[allow(clippy::too_many_lines)]
356 fn try_into(self, schema: Option<&Schema>) -> Result<(PolicySet, Entities), Vec<String>> {
357 let Self {
358 policies,
359 entities,
360 templates,
361 template_instantiations,
362 } = self;
363
364 let policy_set = match policies {
365 PolicySpecification::Concatenated(policies) => match PolicySet::from_str(&policies) {
366 Ok(ps) => Ok(ps),
367 Err(parse_errors) => Err(std::iter::once(
368 "couldn't parse concatenated policies string".to_string(),
369 )
370 .chain(parse_errors.errors_as_strings())
371 .collect()),
372 },
373 PolicySpecification::Map(policies) => {
374 parse_policy_set_from_individual_policies(&policies, templates)
375 }
376 };
377
378 let mut errs = Vec::new();
379
380 let (mut policies, entities) = match (
381 Entities::from_json_value(entities.into(), schema),
382 policy_set,
383 ) {
384 (Ok(entities), Ok(policies)) => (policies, entities),
385 (Ok(_), Err(policy_parse_errors)) => {
386 errs.extend(policy_parse_errors);
387 (PolicySet::new(), Entities::empty())
388 }
389 (Err(e), Ok(_)) => {
390 errs.push(e.to_string());
391 (PolicySet::new(), Entities::empty())
392 }
393 (Err(e), Err(policy_parse_errors)) => {
394 errs.push(e.to_string());
395 errs.extend(policy_parse_errors);
396 (PolicySet::new(), Entities::empty())
397 }
398 };
399
400 if let Some(t_inst_list) = template_instantiations {
401 for instantiation in t_inst_list {
402 match parse_instantiations(&mut policies, instantiation) {
403 Ok(()) => (),
404 Err(err) => errs.extend(err),
405 }
406 }
407 }
408
409 if errs.is_empty() {
410 Ok((policies, entities))
411 } else {
412 Err(errs)
413 }
414 }
415}
416
417fn parse_policy_set_from_individual_policies(
418 policies: &HashMap<String, String>,
419 templates: Option<HashMap<String, String>>,
420) -> Result<PolicySet, Vec<String>> {
421 let mut policy_set = PolicySet::new();
422 let mut errs = Vec::new();
423 for (id, policy_src) in policies {
424 match Policy::parse(Some(id.clone()), policy_src) {
425 Ok(p) => match policy_set.add(p) {
426 Ok(()) => {}
427 Err(err) => {
428 errs.push(format!("couldn't add policy to set due to error: {err}"));
429 }
430 },
431 Err(pes) => errs.extend(
432 std::iter::once(format!("couldn't parse policy with id `{id}`"))
433 .chain(pes.errors_as_strings().into_iter()),
434 ),
435 }
436 }
437
438 if let Some(templates) = templates {
439 for (id, policy_src) in templates {
440 match Template::parse(Some(id.clone()), policy_src) {
441 Ok(p) => match policy_set.add_template(p) {
442 Ok(()) => {}
443 Err(err) => {
444 errs.push(format!("couldn't add policy to set due to error: {err}"));
445 }
446 },
447 Err(pes) => errs.extend(
448 std::iter::once(format!("couldn't parse policy with id `{id}`"))
449 .chain(pes.errors_as_strings().into_iter()),
450 ),
451 }
452 }
453 }
454
455 if errs.is_empty() {
456 Ok(policy_set)
457 } else {
458 Err(errs)
459 }
460}
461
462#[allow(clippy::panic)]
464#[cfg(test)]
465mod test {
466 use super::*;
467 use crate::{frontend::utils::assert_is_failure, EntityUid};
468 use std::collections::HashMap;
469
470 #[test]
471 fn test_slice_convert() {
472 let entities = serde_json::json!(
473 [
474 {
475 "uid" : {
476 "type" : "user",
477 "id" : "alice"
478 },
479 "attrs": { "foo": "bar" },
480 "parents" : [
481 {
482 "type" : "user",
483 "id" : "bob"
484 }
485 ]
486 },
487 {
488 "uid" : {
489 "type" : "user",
490 "id" : "bob"
491 },
492 "attrs": {},
493 "parents": []
494 }
495 ]
496 );
497 let rslice = RecvdSlice {
498 policies: PolicySpecification::Map(HashMap::new()),
499 entities: entities.into(),
500 templates: None,
501 template_instantiations: None,
502 };
503 let (policies, entities) = rslice.try_into(None).expect("parse failed");
504 assert!(policies.is_empty());
505 entities
506 .get(&EntityUid::from_type_name_and_id(
507 "user".parse().unwrap(),
508 "alice".parse().unwrap(),
509 ))
510 .map_or_else(
511 || panic!("Missing user::alice Entity"),
512 |alice| {
513 assert!(entities.is_ancestor_of(
514 &EntityUid::from_type_name_and_id(
515 "user".parse().unwrap(),
516 "bob".parse().unwrap()
517 ),
518 &alice.uid()
519 ));
520 },
521 );
522 }
523
524 #[test]
525 fn test_failure_on_invalid_syntax() {
526 assert_is_failure(
527 &json_is_authorized("iefjieoafiaeosij"),
528 true,
529 "expected value",
530 );
531 }
532
533 #[test]
534 fn test_not_authorized_on_empty_slice() {
535 let call = r#"
536 {
537 "principal": {
538 "type": "User",
539 "id": "alice"
540 },
541 "action": {
542 "type": "Photo",
543 "id": "view"
544 },
545 "resource": {
546 "type": "Photo",
547 "id": "door"
548 },
549 "context": {},
550 "slice": {
551 "policies": {},
552 "entities": []
553 }
554 }
555 "#;
556
557 assert_is_not_authorized(json_is_authorized(call));
558 }
559
560 #[test]
561 fn test_authorized_on_simple_slice() {
562 let call = r#"
563 {
564 "principal": {
565 "type": "User",
566 "id": "alice"
567 },
568 "action": {
569 "type": "Photo",
570 "id": "view"
571 },
572 "resource": {
573 "type": "Photo",
574 "id": "door"
575 },
576 "context": {},
577 "slice": {
578 "policies": {
579 "ID1": "permit(principal == User::\"alice\", action, resource);"
580 },
581 "entities": []
582 }
583 }
584 "#;
585
586 assert_is_authorized(json_is_authorized(call));
587 }
588
589 #[test]
590 fn test_authorized_on_simple_slice_with_string_policies() {
591 let call = r#"
592 {
593 "principal": {
594 "type": "User",
595 "id": "alice"
596 },
597 "action": {
598 "type": "Photo",
599 "id": "view"
600 },
601 "resource": {
602 "type": "Photo",
603 "id": "door"
604 },
605 "context": {},
606 "slice": {
607 "policies": "permit(principal == User::\"alice\", action, resource);",
608 "entities": []
609 }
610 }
611 "#;
612
613 assert_is_authorized(json_is_authorized(call));
614 }
615
616 #[test]
617 fn test_authorized_on_simple_slice_with_context() {
618 let call = r#"
619 {
620 "principal": {
621 "type": "User",
622 "id": "alice"
623 },
624 "action": {
625 "type": "Photo",
626 "id": "view"
627 },
628 "resource": {
629 "type": "Photo",
630 "id": "door"
631 },
632 "context": {
633 "is_authenticated": true,
634 "source_ip": {
635 "__extn" : { "fn" : "ip", "arg" : "222.222.222.222" }
636 }
637 },
638 "slice": {
639 "policies": "permit(principal == User::\"alice\", action, resource) when { context.is_authenticated && context.source_ip.isInRange(ip(\"222.222.222.0/24\")) };",
640 "entities": []
641 }
642 }
643 "#;
644
645 assert_is_authorized(json_is_authorized(call));
646 }
647
648 #[test]
649 fn test_authorized_on_simple_slice_with_attrs_and_parents() {
650 let call = r#"
651 {
652 "principal": {
653 "type": "User",
654 "id": "alice"
655 },
656 "action": {
657 "type": "Photo",
658 "id": "view"
659 },
660 "resource": {
661 "type": "Photo",
662 "id": "door"
663 },
664 "context": {},
665 "slice": {
666 "policies": "permit(principal, action, resource in Folder::\"house\") when { resource.owner == principal };",
667 "entities": [
668 {
669 "uid": {
670 "__entity": {
671 "type": "User",
672 "id": "alice"
673 }
674 },
675 "attrs": {},
676 "parents": []
677 },
678 {
679 "uid": {
680 "__entity": {
681 "type": "Photo",
682 "id": "door"
683 }
684 },
685 "attrs": {
686 "owner": {
687 "__entity": {
688 "type": "User",
689 "id": "alice"
690 }
691 }
692 },
693 "parents": [
694 {
695 "__entity": {
696 "type": "Folder",
697 "id": "house"
698 }
699 }
700 ]
701 },
702 {
703 "uid": {
704 "__entity": {
705 "type": "Folder",
706 "id": "house"
707 }
708 },
709 "attrs": {},
710 "parents": []
711 }
712 ]
713 }
714 }
715 "#;
716
717 assert_is_authorized(json_is_authorized(call));
718 }
719
720 #[test]
721 fn test_authorized_on_multi_policy_slice() {
722 let call = r#"
723 {
724 "principal": {
725 "type": "User",
726 "id": "alice"
727 },
728 "action": {
729 "type": "Photo",
730 "id": "view"
731 },
732 "resource": {
733 "type": "Photo",
734 "id": "door"
735 },
736 "context": {},
737 "slice": {
738 "policies": {
739 "ID0": "permit(principal == User::\"jerry\", action, resource == Photo::\"doorx\");",
740 "ID1": "permit(principal == User::\"tom\", action, resource == Photo::\"doory\");",
741 "ID2": "permit(principal == User::\"alice\", action, resource == Photo::\"door\");"
742 },
743 "entities": []
744 }
745 }
746 "#;
747 assert_is_authorized(json_is_authorized(call));
748 }
749
750 #[test]
751 fn test_authorized_on_multi_policy_slice_with_string_policies() {
752 let call = r#"
753 {
754 "principal": {
755 "type": "User",
756 "id": "alice"
757 },
758 "action": {
759 "type": "Photo",
760 "id": "view"
761 },
762 "resource": {
763 "type": "Photo",
764 "id": "door"
765 },
766 "context": {},
767 "slice": {
768 "policies": "permit(principal, action, resource in Folder::\"house\") when { resource.owner == principal };",
769 "entities": [
770 {
771 "uid": {
772 "__entity": {
773 "type": "User",
774 "id": "alice"
775 }
776 },
777 "attrs": {},
778 "parents": []
779 },
780 {
781 "uid": {
782 "__entity": {
783 "type": "Photo",
784 "id": "door"
785 }
786 },
787 "attrs": {
788 "owner": {
789 "__entity": {
790 "type": "User",
791 "id": "alice"
792 }
793 }
794 },
795 "parents": [
796 {
797 "__entity": {
798 "type": "Folder",
799 "id": "house"
800 }
801 }
802 ]
803 },
804 {
805 "uid": {
806 "__entity": {
807 "type": "Folder",
808 "id": "house"
809 }
810 },
811 "attrs": {},
812 "parents": []
813 }
814 ]
815 }
816 }
817 "#;
818 assert_is_authorized(json_is_authorized(call));
819 }
820
821 #[test]
822 fn test_authorized_on_multi_policy_slice_denies_when_expected() {
823 let call = r#"
824 {
825 "principal": {
826 "type": "User",
827 "id": "alice"
828 },
829 "action": {
830 "type": "Photo",
831 "id": "view"
832 },
833 "resource": {
834 "type": "Photo",
835 "id": "door"
836 },
837 "context": {},
838 "slice": {
839 "policies": {
840 "ID0": "permit(principal, action, resource);",
841 "ID1": "forbid(principal == User::\"alice\", action, resource == Photo::\"door\");"
842 },
843 "entities": []
844 }
845 }
846 "#;
847 assert_is_not_authorized(json_is_authorized(call));
848 }
849
850 #[test]
851 fn test_authorized_on_multi_policy_slice_with_string_policies_denies_when_expected() {
852 let call = r#"
853 {
854 "principal": {
855 "type": "User",
856 "id": "alice"
857 },
858 "action": {
859 "type": "Photo",
860 "id": "view"
861 },
862 "resource": {
863 "type": "Photo",
864 "id": "door"
865 },
866 "context": {},
867 "slice": {
868 "policies": "permit(principal, action, resource);forbid(principal == User::\"alice\", action, resource);",
869 "entities": []
870 }
871 }
872 "#;
873
874 assert_is_not_authorized(json_is_authorized(call));
875 }
876
877 #[test]
878 fn test_authorized_with_template_as_policy_should_fail() {
879 let call = r#"
880 {
881 "principal": {
882 "type": "User",
883 "id": "alice"
884 },
885 "action": {
886 "type": "Photo",
887 "id": "view"
888 },
889 "resource": {
890 "type": "Photo",
891 "id": "door"
892 },
893 "context": {},
894 "slice": {
895 "policies": "permit(principal == ?principal, action, resource);",
896 "entities": [],
897 "templates": {}
898 }
899 }
900 "#;
901 assert_is_not_authorized(json_is_authorized(call));
902 }
903
904 #[test]
905 fn test_authorized_with_template_should_fail() {
906 let call = r#"
907 {
908 "principal": {
909 "type": "User",
910 "id": "alice"
911 },
912 "action": {
913 "type": "Photo",
914 "id": "view"
915 },
916 "resource": {
917 "type": "Photo",
918 "id": "door"
919 },
920 "context": {},
921 "slice": {
922 "policies": {},
923 "entities": [],
924 "templates": {
925 "ID0": "permit(principal == ?principal, action, resource);"
926 }
927 }
928 }
929 "#;
930 assert_is_not_authorized(json_is_authorized(call));
931 }
932
933 #[test]
934 fn test_authorized_with_template_instantiation() {
935 let call = r#"
936 {
937 "principal": {
938 "type": "User",
939 "id": "alice"
940 },
941 "action": {
942 "type": "Photo",
943 "id": "view"
944 },
945 "resource": {
946 "type": "Photo",
947 "id": "door"
948 },
949 "context": {},
950 "slice": {
951 "policies": {},
952 "entities": [],
953 "templates": {
954 "ID0": "permit(principal == ?principal, action, resource);"
955 },
956 "template_instantiations": [
957 {
958 "template_id": "ID0",
959 "result_policy_id": "ID0_User_alice",
960 "instantiations": [
961 {
962 "slot": "?principal",
963 "value": {
964 "ty": "User",
965 "eid": "alice"
966 }
967 }
968 ]
969 }
970 ]
971 }
972 }
973 "#;
974 assert_is_authorized(json_is_authorized(call));
975 }
976
977 #[test]
978 fn test_authorized_fails_on_policy_collision_with_template() {
979 let call = r#"{
980 "principal" : {
981 "type" : "User",
982 "id" : "alice"
983 },
984 "action" : {
985 "type" : "Action",
986 "id" : "view"
987 },
988 "resource" : {
989 "type" : "Photo",
990 "id" : "door"
991 },
992 "context" : {},
993 "slice" : {
994 "policies" : { "ID0": "permit(principal, action, resource);" },
995 "entities" : [],
996 "templates" : { "ID0": "permit(principal == ?principal, action, resource);" },
997 "template_instantiations" : []
998 }
999 }"#;
1000 assert_is_failure(
1001 &json_is_authorized(call),
1002 false,
1003 "couldn't add policy to set due to error: duplicate template or policy id `ID0`",
1004 );
1005 }
1006
1007 #[test]
1008 fn test_authorized_fails_on_duplicate_instantiations_ids() {
1009 let call = r#"{
1010 "principal" : {
1011 "type" : "User",
1012 "id" : "alice"
1013 },
1014 "action" : {
1015 "type" : "Action",
1016 "id" : "view"
1017 },
1018 "resource" : {
1019 "type" : "Photo",
1020 "id" : "door"
1021 },
1022 "context" : {},
1023 "slice" : {
1024 "policies" : {},
1025 "entities" : [],
1026 "templates" : { "ID0": "permit(principal == ?principal, action, resource);" },
1027 "template_instantiations" : [
1028 {
1029 "template_id" : "ID0",
1030 "result_policy_id" : "ID1",
1031 "instantiations" : [
1032 {
1033 "slot": "?principal",
1034 "value": { "ty" : "User", "eid" : "alice" }
1035 }
1036 ]
1037 },
1038 {
1039 "template_id" : "ID0",
1040 "result_policy_id" : "ID1",
1041 "instantiations" : [
1042 {
1043 "slot": "?principal",
1044 "value": { "ty" : "User", "eid" : "alice" }
1045 }
1046 ]
1047 }
1048 ]
1049 }
1050 }"#;
1051 assert_is_failure(
1052 &json_is_authorized(call),
1053 false,
1054 "Error instantiating template: unable to link template: template-linked policy id `ID1` conflicts with an existing policy id",
1055 );
1056 }
1057
1058 #[test]
1059 fn test_authorized_fails_on_template_instantiation_collision_with_template() {
1060 let call = r#"{
1061 "principal" : {
1062 "type" : "User",
1063 "id" : "alice"
1064 },
1065 "action" : {
1066 "type" : "Action",
1067 "id" : "view"
1068 },
1069 "resource" : {
1070 "type" : "Photo",
1071 "id" : "door"
1072 },
1073 "context" : {},
1074 "slice" : {
1075 "policies" : {},
1076 "entities" : [],
1077 "templates" : { "ID0": "permit(principal == ?principal, action, resource);" },
1078 "template_instantiations" : [
1079 {
1080 "template_id" : "ID0",
1081 "result_policy_id" : "ID0",
1082 "instantiations" : [
1083 {
1084 "slot": "?principal",
1085 "value": { "ty" : "User", "eid" : "alice" }
1086 }
1087 ]
1088 }
1089 ]
1090 }
1091 }"#;
1092 assert_is_failure(
1093 &json_is_authorized(call),
1094 false,
1095 "Error instantiating template: unable to link template: template-linked policy id `ID0` conflicts with an existing policy id",
1096 );
1097 }
1098
1099 #[test]
1100 fn test_authorized_fails_on_template_instantiation_collision_with_policy() {
1101 let call = r#"{
1102 "principal" : {
1103 "type" : "User",
1104 "id" : "alice"
1105 },
1106 "action" : {
1107 "type" : "Action",
1108 "id" : "view"
1109 },
1110 "resource" : {
1111 "type" : "Photo",
1112 "id" : "door"
1113 },
1114 "context" : {},
1115 "slice" : {
1116 "policies" : { "ID1": "permit(principal, action, resource);" },
1117 "entities" : [],
1118 "templates" : { "ID0": "permit(principal == ?principal, action, resource);" },
1119 "template_instantiations" : [
1120 {
1121 "template_id" : "ID0",
1122 "result_policy_id" : "ID1",
1123 "instantiations" : [
1124 {
1125 "slot": "?principal",
1126 "value": { "ty" : "User", "eid" : "alice" }
1127 }
1128 ]
1129 }
1130 ]
1131 }
1132 }"#;
1133 assert_is_failure(
1134 &json_is_authorized(call),
1135 false,
1136 "Error instantiating template: unable to link template: template-linked policy id `ID1` conflicts with an existing policy id",
1137 );
1138 }
1139
1140 fn assert_is_authorized(result: InterfaceResult) {
1141 match result {
1142 InterfaceResult::Success { result } => {
1143 let parsed_result: AuthorizationAnswer =
1144 serde_json::from_str(result.as_str()).unwrap();
1145 match parsed_result {
1146 AuthorizationAnswer::ParseFailed { .. } => {
1147 panic!("expected parse to succeed, but got {parsed_result:?}")
1148 }
1149 AuthorizationAnswer::Success { response } => {
1150 assert_eq!(response.decision, Decision::Allow);
1151 assert_eq!(response.diagnostics.errors.len(), 0);
1152 }
1153 }
1154 }
1155 InterfaceResult::Failure { .. } => {
1156 panic!("Expected a successful response, not {result:?}");
1157 }
1158 }
1159 }
1160
1161 fn assert_is_not_authorized(result: InterfaceResult) {
1162 match result {
1163 InterfaceResult::Success { result } => {
1164 let parsed_result: AuthorizationAnswer =
1165 serde_json::from_str(result.as_str()).unwrap();
1166 match parsed_result {
1167 AuthorizationAnswer::ParseFailed { .. } => {
1168 panic!("expected parse to succeed, but got {parsed_result:?}")
1169 }
1170 AuthorizationAnswer::Success { response } => {
1171 assert_eq!(response.decision, Decision::Deny);
1172 assert_eq!(response.diagnostics.errors.len(), 0);
1173 }
1174 }
1175 }
1176 InterfaceResult::Failure { .. } => {
1177 panic!("Expected a successful response, not {result:?}");
1178 }
1179 }
1180 }
1181
1182 #[test]
1183 fn test_authorized_fails_on_duplicate_policy_ids() {
1184 let call = r#"{
1185 "principal" : "User::\"alice\"",
1186 "action" : "Photo::\"view\"",
1187 "resource" : "Photo::\"door\"",
1188 "context" : {},
1189 "slice" : {
1190 "policies" : {
1191 "ID0": "permit(principal, action, resource);",
1192 "ID0": "permit(principal, action, resource);"
1193 },
1194 "entities" : [],
1195 "templates" : {},
1196 "template_instantiations" : [ ]
1197 }
1198 }"#;
1199 assert_is_failure(&json_is_authorized(call), true, "no duplicate IDs");
1200 }
1201
1202 #[test]
1203 fn test_authorized_fails_on_duplicate_template_ids() {
1204 let call = r#"{
1205 "principal" : "User::\"alice\"",
1206 "action" : "Photo::\"view\"",
1207 "resource" : "Photo::\"door\"",
1208 "context" : {},
1209 "slice" : {
1210 "policies" : {},
1211 "entities" : [],
1212 "templates" : {
1213 "ID0": "permit(principal == ?principal, action, resource);",
1214 "ID0": "permit(principal == ?principal, action, resource);"
1215 },
1216 "template_instantiations" : [ ]
1217 }
1218 }"#;
1219 assert_is_failure(&json_is_authorized(call), true, "found duplicate key");
1220 }
1221
1222 #[test]
1223 fn test_authorized_fails_on_duplicate_slot_instantiation1() {
1224 let call = r#"{
1225 "principal" : "User::\"alice\"",
1226 "action" : "Photo::\"view\"",
1227 "resource" : "Photo::\"door\"",
1228 "context" : {},
1229 "slice" : {
1230 "policies" : {},
1231 "entities" : [],
1232 "templates" : { "ID0": "permit(principal == ?principal, action, resource);" },
1233 "template_instantiations" : [
1234 {
1235 "template_id" : "ID0",
1236 "result_policy_id" : "ID1",
1237 "instantiations" : [
1238 {
1239 "slot": "?principal",
1240 "value": { "ty" : "User", "eid" : "alice" }
1241 },
1242 {
1243 "slot": "?principal",
1244 "value": { "ty" : "User", "eid" : "alice" }
1245 }
1246 ]
1247 }
1248 ]
1249 }
1250 }"#;
1251 assert_is_failure(
1252 &json_is_authorized(call),
1253 true,
1254 "duplicate instantiations of the slot(s): `?principal`",
1255 );
1256 }
1257
1258 #[test]
1259 fn test_authorized_fails_on_duplicate_slot_instantiation2() {
1260 let call = r#"{
1261 "principal" : "User::\"alice\"",
1262 "action" : "Photo::\"view\"",
1263 "resource" : "Photo::\"door\"",
1264 "context" : {},
1265 "slice" : {
1266 "policies" : {},
1267 "entities" : [],
1268 "templates" : { "ID0": "permit(principal == ?principal, action, resource);" },
1269 "template_instantiations" : [
1270 {
1271 "template_id" : "ID0",
1272 "result_policy_id" : "ID1",
1273 "instantiations" : [
1274 {
1275 "slot": "?principal",
1276 "value": { "ty" : "User", "eid" : "alice" }
1277 },
1278 {
1279 "slot" : "?resource",
1280 "value" : { "ty" : "Box", "eid" : "box" }
1281 },
1282 {
1283 "slot": "?principal",
1284 "value": { "ty" : "User", "eid" : "alice" }
1285 }
1286 ]
1287 }
1288 ]
1289 }
1290 }"#;
1291 assert_is_failure(
1292 &json_is_authorized(call),
1293 true,
1294 "duplicate instantiations of the slot(s): `?principal`",
1295 );
1296 }
1297
1298 #[test]
1299 fn test_authorized_fails_on_duplicate_slot_instantiation3() {
1300 let call = r#"{
1301 "principal" : "User::\"alice\"",
1302 "action" : "Photo::\"view\"",
1303 "resource" : "Photo::\"door\"",
1304 "context" : {},
1305 "slice" : {
1306 "policies" : {},
1307 "entities" : [],
1308 "templates" : { "ID0": "permit(principal == ?principal, action, resource);" },
1309 "template_instantiations" : [
1310 {
1311 "template_id" : "ID0",
1312 "result_policy_id" : "ID1",
1313 "instantiations" : [
1314 {
1315 "slot": "?principal",
1316 "value": { "ty" : "User", "eid" : "alice" }
1317 },
1318 {
1319 "slot" : "?resource",
1320 "value" : { "ty" : "Box", "eid" : "box" }
1321 },
1322 {
1323 "slot": "?principal",
1324 "value": { "ty" : "Team", "eid" : "bob" }
1325 },
1326 {
1327 "slot" : "?resource",
1328 "value" : { "ty" : "Box", "eid" : "box2" }
1329 }
1330 ]
1331 }
1332 ]
1333 }
1334 }"#;
1335 assert_is_failure(
1336 &json_is_authorized(call),
1337 true,
1338 "duplicate instantiations of the slot(s): `?principal`, `?resource`",
1339 );
1340 }
1341
1342 #[test]
1343 fn test_authorized_fails_duplicate_entity_uid() {
1344 let call = r#"{
1345 "principal" : {
1346 "type" : "User",
1347 "id" : "alice"
1348 },
1349 "action" : {
1350 "type" : "Photo",
1351 "id" : "view"
1352 },
1353 "resource" : {
1354 "type" : "Photo",
1355 "id" : "door"
1356 },
1357 "context" : {},
1358 "slice" : {
1359 "policies" : {},
1360 "entities" : [
1361 {
1362 "uid": {
1363 "type" : "User",
1364 "id" : "alice"
1365 },
1366 "attrs": {},
1367 "parents": []
1368 },
1369 {
1370 "uid": {
1371 "type" : "User",
1372 "id" : "alice"
1373 },
1374 "attrs": {},
1375 "parents": []
1376 }
1377 ],
1378 "templates" : {},
1379 "template_instantiations" : []
1380 }
1381 }"#;
1382 assert_is_failure(
1383 &json_is_authorized(call),
1384 false,
1385 r#"duplicate entity entry `User::"alice"`"#,
1386 );
1387 }
1388
1389 #[test]
1390 fn test_authorized_fails_duplicate_context_key() {
1391 let call = r#"{
1392 "principal" : {
1393 "type" : "User",
1394 "id" : "alice"
1395 },
1396 "action" : {
1397 "type" : "Photo",
1398 "id" : "view"
1399 },
1400 "resource" : {
1401 "type" : "Photo",
1402 "id" : "door"
1403 },
1404 "context" : {
1405 "is_authenticated": true,
1406 "is_authenticated": false
1407 },
1408 "slice" : {
1409 "policies" : {},
1410 "entities" : [],
1411 "templates" : {},
1412 "template_instantiations" : []
1413 }
1414 }"#;
1415 assert_is_failure(&json_is_authorized(call), true, "found duplicate key");
1416 }
1417}