cedar_policy/frontend/
is_authorized.rs

1/*
2 * Copyright 2022-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! This module contains the `json_is_authorized` entry point that other language
18//! FFI's can call in order to use Cedar functionality
19#![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    /// Per-thread authorizer instance, initialized on first use
39    static AUTHORIZER: Authorizer = Authorizer::new();
40);
41
42/// Construct and ask the authorizer the request.
43fn 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
56/// public string-based JSON interfaced to be invoked by FFIs. In the policies portion of
57/// the `RecvdSlice`, you can either pass a `Map<String, String>` where the values are all single policies,
58/// or a single String which is a concatenation of multiple policies. If you choose the latter,
59/// policy id's will be auto-generated for you in the format `policyX` where X is a Whole Number (zero or a positive int)
60pub 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/// Interface version of a `Response` that uses `InterfaceDiagnostics` for simpler (de)serialization
73#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
74pub struct InterfaceResponse {
75    /// Authorization decision
76    decision: Decision,
77    /// Diagnostics providing more information on how this decision was reached
78    diagnostics: InterfaceDiagnostics,
79}
80
81/// Interface version of `Diagnostics` that stores error messages as strings for simpler (de)serialization
82#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
83pub struct InterfaceDiagnostics {
84    /// `PolicyId`s of the policies that contributed to the decision.
85    /// If no policies applied to the request, this set will be empty.
86    reason: HashSet<PolicyId>,
87    /// Set of error messages that occurred
88    errors: HashSet<String>,
89}
90
91impl InterfaceResponse {
92    /// Construct an `InterfaceResponse`
93    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    /// Get the authorization decision
101    pub fn decision(&self) -> Decision {
102        self.decision
103    }
104
105    /// Get the authorization diagnostics
106    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    /// Get the policies that contributed to the decision
127    pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
128        self.reason.iter()
129    }
130
131    /// Get the errors
132    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    /// Optional schema in JSON format.
153    /// If present, this will inform the parsing: for instance, it will allow
154    /// `__entity` and `__extn` escapes to be implicit, and it will error if
155    /// attributes have the wrong types (e.g., string instead of integer).
156    #[serde(rename = "schema")]
157    schema: Option<JsonValueWithNoDuplicateKeys>,
158    /// If this is `true` and a schema is provided, perform request validation.
159    /// If this is `false`, the schema will only be used for schema-based
160    /// parsing of `context`, and not for request validation.
161    /// If a schema is not provided, this option has no effect.
162    #[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///
217/// Entity UID as strings.
218///
219#[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 to fill in
234    template_id: String,
235
236    /// Policy id for resulting concrete policy instance
237    result_policy_id: String,
238
239    /// List of strings to fill in all slots in policy template "template_id".
240    /// (slot, String)
241    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/// Error returned for duplicate link ids in a template instantiation
250#[derive(Debug, Clone, Error)]
251pub enum DuplicateLinkError {
252    /// Duplicate instantiations for the same slot
253    #[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/// policies must either be a single policy per entry, or only one entry with more than one policy
284#[serde_as]
285#[derive(Debug, Serialize, Deserialize)]
286struct RecvdSlice {
287    policies: PolicySpecification,
288    /// JSON object containing the entities data, in "natural JSON" form -- same
289    /// format as expected by EntityJsonParser
290    entities: JsonValueWithNoDuplicateKeys,
291
292    /// Optional template policies.
293    #[serde_as(as = "Option<MapPreventDuplicates<_, _>>")]
294    templates: Option<HashMap<String, String>>,
295
296    /// Optional template instantiations.
297    /// List of instantiations, one per
298    /// If present, instantiate policies
299    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// PANIC SAFETY unit tests
463#[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}