From 9aafdfe1a5a213eeb785f35cbe5318554103025b Mon Sep 17 00:00:00 2001 From: shaobo-he-aws <130499339+shaobo-he-aws@users.noreply.github.com> Date: Thu, 15 Aug 2024 13:21:15 -0400 Subject: [PATCH] Cherry-pick #1141 to release/3.3.x (#1143) Signed-off-by: Aaron Eline Co-authored-by: Aaron Eline --- cedar-policy-validator/src/schema.rs | 543 ++++++++++++++++++- cedar-policy-validator/src/schema/action.rs | 55 ++ cedar-policy-validator/src/types.rs | 2 +- cedar-policy/src/api.rs | 557 +++++++++++++++++++- 4 files changed, 1154 insertions(+), 3 deletions(-) diff --git a/cedar-policy-validator/src/schema.rs b/cedar-policy-validator/src/schema.rs index 80f745aa7..dd9371218 100644 --- a/cedar-policy-validator/src/schema.rs +++ b/cedar-policy-validator/src/schema.rs @@ -149,7 +149,85 @@ impl TryFrom for ValidatorSchema { } impl ValidatorSchema { - // Create a ValidatorSchema without any entity types or actions ids. + /// Returns an iterator over every entity type that can be a principal for any action in this schema + pub fn principals(&self) -> impl Iterator { + self.action_ids + .values() + .flat_map(ValidatorActionId::principals) + } + + /// Returns an iterator over every entity type that can be a resource for any action in this schema + pub fn resources(&self) -> impl Iterator { + self.action_ids + .values() + .flat_map(ValidatorActionId::resources) + } + + /// Returns an iterator over every entity type that can be a principal for `action` in this schema + /// + /// # Errors + /// + /// Returns [`None`] if `action` is not found in the schema + pub fn principals_for_action( + &self, + action: &EntityUID, + ) -> Option> { + self.action_ids + .get(action) + .map(ValidatorActionId::principals) + } + + /// Returns an iterator over every entity type that can be a resource for `action` in this schema + /// + /// # Errors + /// + /// Returns [`None`] if `action` is not found in the schema + pub fn resources_for_action( + &self, + action: &EntityUID, + ) -> Option> { + self.action_ids + .get(action) + .map(ValidatorActionId::resources) + } + + /// Returns an iterator over all the entity types that can be a parent of `ty` + /// + /// # Errors + /// + /// Returns [`None`] if the `ty` is not found in the schema + pub fn ancestors<'a>(&'a self, ty: &'a Name) -> Option + 'a> { + if self.entity_types.contains_key(ty) { + Some(self.entity_types.values().filter_map(|ety| { + if ety.descendants.contains(ty) { + Some(&ety.name) + } else { + None + } + })) + } else { + None + } + } + + /// Returns an iterator over all the action groups defined in this schema + pub fn action_groups(&self) -> impl Iterator { + self.action_ids.values().filter_map(|action| { + if action.descendants.is_empty() { + None + } else { + Some(&action.name) + } + }) + } + + /// Returns an iterator over all actions defined in this schema + pub fn actions(&self) -> impl Iterator { + self.action_ids.keys() + } + + /// Create a [`ValidatorSchema`] without any definitions (of entity types, + /// common types, or actions). pub fn empty() -> ValidatorSchema { Self { entity_types: HashMap::new(), @@ -2531,3 +2609,466 @@ mod test_resolver { assert_matches!(res, Err(SchemaError::CycleInCommonTypeReferences(_))); } } + +#[cfg(test)] +mod test_access { + use super::*; + + fn schema() -> ValidatorSchema { + let src = r#" + type Task = { + "id": Long, + "name": String, + "state": String, +}; + +type Tasks = Set; +entity List in [Application] = { + "editors": Team, + "name": String, + "owner": User, + "readers": Team, + "tasks": Tasks, +}; +entity Application; +entity User in [Team, Application] = { + "joblevel": Long, + "location": String, +}; + +entity CoolList; + +entity Team in [Team, Application]; + +action Read, Write, Create; + +action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo { + principal: [User], + resource : [List] +}; + +action GetList in Read appliesTo { + principal : [User], + resource : [List, CoolList] +}; + +action GetLists in Read appliesTo { + principal : [User], + resource : [Application] +}; + +action CreateList in Create appliesTo { + principal : [User], + resource : [Application] +}; + + "#; + + ValidatorSchema::from_str_natural(src, Extensions::all_available()) + .unwrap() + .0 + } + + #[test] + fn principals() { + let schema = schema(); + let principals = schema.principals().collect::>(); + assert_eq!(principals.len(), 1); + let user: EntityType = EntityType::Specified("User".parse().unwrap()); + assert!(principals.contains(&user)); + let principals = schema.principals().collect::>(); + assert!(principals.len() > 1); + assert!(principals.iter().all(|ety| **ety == user)); + } + + #[test] + fn empty_schema_principals_and_resources() { + let empty: ValidatorSchema = + ValidatorSchema::from_str_natural("", Extensions::all_available()) + .unwrap() + .0; + assert!(empty.principals().collect::>().is_empty()); + assert!(empty.resources().collect::>().is_empty()); + } + + #[test] + fn resources() { + let schema = schema(); + let resources = schema.resources().cloned().collect::>(); + let expected: HashSet = HashSet::from([ + EntityType::Specified("List".parse().unwrap()), + EntityType::Specified("Application".parse().unwrap()), + EntityType::Specified("CoolList".parse().unwrap()), + ]); + assert_eq!(resources, expected); + } + + #[test] + fn principals_for_action() { + let schema = schema(); + let delete_list: EntityUID = r#"Action::"DeleteList""#.parse().unwrap(); + let delete_user: EntityUID = r#"Action::"DeleteUser""#.parse().unwrap(); + let got = schema + .principals_for_action(&delete_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!(got, vec![EntityType::Specified("User".parse().unwrap())]); + assert!(schema.principals_for_action(&delete_user).is_none()); + } + + #[test] + fn resources_for_action() { + let schema = schema(); + let delete_list: EntityUID = r#"Action::"DeleteList""#.parse().unwrap(); + let delete_user: EntityUID = r#"Action::"DeleteUser""#.parse().unwrap(); + let create_list: EntityUID = r#"Action::"CreateList""#.parse().unwrap(); + let get_list: EntityUID = r#"Action::"GetList""#.parse().unwrap(); + let got = schema + .resources_for_action(&delete_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!(got, vec![EntityType::Specified("List".parse().unwrap())]); + let got = schema + .resources_for_action(&create_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!( + got, + vec![EntityType::Specified("Application".parse().unwrap())] + ); + let got = schema + .resources_for_action(&get_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!( + got, + HashSet::from([ + EntityType::Specified("List".parse().unwrap()), + EntityType::Specified("CoolList".parse().unwrap()) + ]) + ); + assert!(schema.principals_for_action(&delete_user).is_none()); + } + + #[test] + fn principal_parents() { + let schema = schema(); + let user: Name = "User".parse().unwrap(); + let parents = schema + .ancestors(&user) + .unwrap() + .cloned() + .collect::>(); + let expected = HashSet::from(["Team".parse().unwrap(), "Application".parse().unwrap()]); + assert_eq!(parents, expected); + let parents = schema + .ancestors(&"List".parse().unwrap()) + .unwrap() + .cloned() + .collect::>(); + let expected = HashSet::from(["Application".parse().unwrap()]); + assert_eq!(parents, expected); + assert!(schema.ancestors(&"Foo".parse().unwrap()).is_none()); + let parents = schema + .ancestors(&"CoolList".parse().unwrap()) + .unwrap() + .cloned() + .collect::>(); + let expected = HashSet::from([]); + assert_eq!(parents, expected); + } + + #[test] + fn action_groups() { + let schema = schema(); + let groups = schema.action_groups().cloned().collect::>(); + let expected = ["Read", "Write", "Create"] + .into_iter() + .map(|ty| format!("Action::\"{ty}\"").parse().unwrap()) + .collect::>(); + assert_eq!(groups, expected); + } + + #[test] + fn actions() { + let schema = schema(); + let actions = schema.actions().cloned().collect::>(); + let expected = [ + "Read", + "Write", + "Create", + "DeleteList", + "EditShare", + "UpdateList", + "CreateTask", + "UpdateTask", + "DeleteTask", + "GetList", + "GetLists", + "CreateList", + ] + .into_iter() + .map(|ty| format!("Action::\"{ty}\"").parse().unwrap()) + .collect::>(); + assert_eq!(actions, expected); + } + + #[test] + fn entities() { + let schema = schema(); + let entities = schema + .entity_types() + .map(|(ty, _)| ty) + .cloned() + .collect::>(); + let expected = ["List", "Application", "User", "CoolList", "Team"] + .into_iter() + .map(|ty| ty.parse().unwrap()) + .collect::>(); + assert_eq!(entities, expected); + } +} + +#[cfg(test)] +mod test_access_namespace { + use super::*; + + fn schema() -> ValidatorSchema { + let src = r#" + namespace Foo { + type Task = { + "id": Long, + "name": String, + "state": String, +}; + +type Tasks = Set; +entity List in [Application] = { + "editors": Team, + "name": String, + "owner": User, + "readers": Team, + "tasks": Tasks, +}; +entity Application; +entity User in [Team, Application] = { + "joblevel": Long, + "location": String, +}; + +entity CoolList; + +entity Team in [Team, Application]; + +action Read, Write, Create; + +action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo { + principal: [User], + resource : [List] +}; + +action GetList in Read appliesTo { + principal : [User], + resource : [List, CoolList] +}; + +action GetLists in Read appliesTo { + principal : [User], + resource : [Application] +}; + +action CreateList in Create appliesTo { + principal : [User], + resource : [Application] +}; + } + + "#; + + ValidatorSchema::from_str_natural(src, Extensions::all_available()) + .unwrap() + .0 + } + + #[test] + fn principals() { + let schema = schema(); + let principals = schema.principals().collect::>(); + assert_eq!(principals.len(), 1); + let user: EntityType = EntityType::Specified("Foo::User".parse().unwrap()); + assert!(principals.contains(&user)); + let principals = schema.principals().collect::>(); + assert!(principals.len() > 1); + assert!(principals.iter().all(|ety| **ety == user)); + } + + #[test] + fn empty_schema_principals_and_resources() { + let empty: ValidatorSchema = + ValidatorSchema::from_str_natural("", Extensions::all_available()) + .unwrap() + .0; + assert!(empty.principals().collect::>().is_empty()); + assert!(empty.resources().collect::>().is_empty()); + } + + #[test] + fn resources() { + let schema = schema(); + let resources = schema.resources().cloned().collect::>(); + let expected: HashSet = HashSet::from([ + EntityType::Specified("Foo::List".parse().unwrap()), + EntityType::Specified("Foo::Application".parse().unwrap()), + EntityType::Specified("Foo::CoolList".parse().unwrap()), + ]); + assert_eq!(resources, expected); + } + + #[test] + fn principals_for_action() { + let schema = schema(); + let delete_list: EntityUID = r#"Foo::Action::"DeleteList""#.parse().unwrap(); + let delete_user: EntityUID = r#"Foo::Action::"DeleteUser""#.parse().unwrap(); + let got = schema + .principals_for_action(&delete_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!( + got, + vec![EntityType::Specified("Foo::User".parse().unwrap())] + ); + assert!(schema.principals_for_action(&delete_user).is_none()); + } + + #[test] + fn resources_for_action() { + let schema = schema(); + let delete_list: EntityUID = r#"Foo::Action::"DeleteList""#.parse().unwrap(); + let delete_user: EntityUID = r#"Foo::Action::"DeleteUser""#.parse().unwrap(); + let create_list: EntityUID = r#"Foo::Action::"CreateList""#.parse().unwrap(); + let get_list: EntityUID = r#"Foo::Action::"GetList""#.parse().unwrap(); + let got = schema + .resources_for_action(&delete_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!( + got, + vec![EntityType::Specified("Foo::List".parse().unwrap())] + ); + let got = schema + .resources_for_action(&create_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!( + got, + vec![EntityType::Specified("Foo::Application".parse().unwrap())] + ); + let got = schema + .resources_for_action(&get_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!( + got, + HashSet::from([ + EntityType::Specified("Foo::List".parse().unwrap()), + EntityType::Specified("Foo::CoolList".parse().unwrap()) + ]) + ); + assert!(schema.principals_for_action(&delete_user).is_none()); + } + + #[test] + fn principal_parents() { + let schema = schema(); + let user: Name = "Foo::User".parse().unwrap(); + let parents = schema + .ancestors(&user) + .unwrap() + .cloned() + .collect::>(); + let expected = HashSet::from([ + "Foo::Team".parse().unwrap(), + "Foo::Application".parse().unwrap(), + ]); + assert_eq!(parents, expected); + let parents = schema + .ancestors(&"Foo::List".parse().unwrap()) + .unwrap() + .cloned() + .collect::>(); + let expected = HashSet::from(["Foo::Application".parse().unwrap()]); + assert_eq!(parents, expected); + assert!(schema.ancestors(&"Foo::Foo".parse().unwrap()).is_none()); + let parents = schema + .ancestors(&"Foo::CoolList".parse().unwrap()) + .unwrap() + .cloned() + .collect::>(); + let expected = HashSet::from([]); + assert_eq!(parents, expected); + } + + #[test] + fn action_groups() { + let schema = schema(); + let groups = schema.action_groups().cloned().collect::>(); + let expected = ["Read", "Write", "Create"] + .into_iter() + .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap()) + .collect::>(); + assert_eq!(groups, expected); + } + + #[test] + fn actions() { + let schema = schema(); + let actions = schema.actions().cloned().collect::>(); + let expected = [ + "Read", + "Write", + "Create", + "DeleteList", + "EditShare", + "UpdateList", + "CreateTask", + "UpdateTask", + "DeleteTask", + "GetList", + "GetLists", + "CreateList", + ] + .into_iter() + .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap()) + .collect::>(); + assert_eq!(actions, expected); + } + + #[test] + fn entities() { + let schema = schema(); + let entities = schema + .entity_types() + .map(|(ty, _)| ty) + .cloned() + .collect::>(); + let expected = [ + "Foo::List", + "Foo::Application", + "Foo::User", + "Foo::CoolList", + "Foo::Team", + ] + .into_iter() + .map(|ty| ty.parse().unwrap()) + .collect::>(); + assert_eq!(entities, expected); + } +} diff --git a/cedar-policy-validator/src/schema/action.rs b/cedar-policy-validator/src/schema/action.rs index e2ea16bbf..d8295039d 100644 --- a/cedar-policy-validator/src/schema/action.rs +++ b/cedar-policy-validator/src/schema/action.rs @@ -60,6 +60,16 @@ pub struct ValidatorActionId { } impl ValidatorActionId { + /// Returns an iterator over all the principals that this action applies to + pub fn principals(&self) -> impl Iterator { + self.applies_to.principal_apply_spec.iter() + } + + /// Returns an iterator over all the resources that this action applies to + pub fn resources(&self) -> impl Iterator { + self.applies_to.resource_apply_spec.iter() + } + /// The `Type` that this action requires for its context. /// /// This always returns a closed record type. @@ -147,3 +157,48 @@ impl ValidatorApplySpec { self.resource_apply_spec.iter() } } + +#[cfg(test)] +mod test { + use super::*; + + fn make_action() -> ValidatorActionId { + ValidatorActionId { + name: r#"Action::"foo""#.parse().unwrap(), + applies_to: ValidatorApplySpec { + principal_apply_spec: HashSet::from([ + // Make sure duplicates are handled as expected + EntityType::Specified("User".parse().unwrap()), + EntityType::Specified("User".parse().unwrap()), + ]), + resource_apply_spec: HashSet::from([ + EntityType::Specified("App".parse().unwrap()), + EntityType::Specified("File".parse().unwrap()), + ]), + }, + descendants: HashSet::new(), + context: Type::any_record(), + attribute_types: Attributes::default(), + attributes: BTreeMap::default(), + } + } + + #[test] + fn test_resources() { + let a = make_action(); + let got = a.resources().cloned().collect::>(); + let expected = HashSet::from([ + EntityType::Specified("App".parse().unwrap()), + EntityType::Specified("File".parse().unwrap()), + ]); + assert_eq!(got, expected); + } + + #[test] + fn test_principals() { + let a = make_action(); + let got = a.principals().cloned().collect::>(); + let expected: [EntityType; 1] = [EntityType::Specified("User".parse().unwrap())]; + assert_eq!(got, &expected); + } +} diff --git a/cedar-policy-validator/src/types.rs b/cedar-policy-validator/src/types.rs index a1f0fceb6..1eb6dcd66 100644 --- a/cedar-policy-validator/src/types.rs +++ b/cedar-policy-validator/src/types.rs @@ -1039,7 +1039,7 @@ impl EntityLUB { /// Represents the attributes of a record or entity type. Each attribute has an /// identifier, a flag indicating weather it is required, and a type. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Default)] pub struct Attributes { pub attrs: BTreeMap, } diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 1e538e685..5878e1064 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -22,9 +22,9 @@ )] pub use ast::Effect; pub use authorizer::Decision; -use cedar_policy_core::ast; #[cfg(feature = "partial-eval")] use cedar_policy_core::ast::BorrowedRestrictedExpr; +use cedar_policy_core::ast::{self, EntityType}; use cedar_policy_core::ast::{ ContextCreationError, ExprConstructionError, Integer, RestrictedExprParseError, }; // `ContextCreationError` is unsuitable for `pub use` because it contains internal types like `RestrictedExpr` @@ -1564,6 +1564,132 @@ impl Schema { pub fn action_entities(&self) -> Result { Ok(Entities(self.0.action_entities()?)) } + + /// Returns an iterator over every entity type that can be a principal for any action in this schema + /// + /// Note: this iterator may contain duplicates. + /// + /// # Examples + /// Here's an example of using a [`std::collections::HashSet`] to get a de-duplicated set of principals + /// ``` + /// use std::collections::HashSet; + /// use cedar_policy::Schema; + /// let schema : Schema = Schema::from_cedarschema_str(r#" + /// entity User; + /// entity Folder; + /// action Access appliesTo { + /// principal : User, + /// resource : Folder, + /// }; + /// action Delete appliesTo { + /// principal : User, + /// resource : Folder, + /// }; + /// "#).unwrap().0; + /// let principals = schema.principals().collect::>(); + /// assert_eq!(principals, HashSet::from([&"User".parse().unwrap()])); + /// ``` + pub fn principals(&self) -> impl Iterator { + self.0.principals().filter_map(|ty| match ty { + EntityType::Specified(name) => Some(EntityTypeName::ref_cast(name)), + EntityType::Unspecified => None, + }) + } + + /// Returns an iterator over every entity type that can be a resource for any action in this schema + /// + /// Note: this iterator may contain duplicates. + /// # Examples + /// Here's an example of using a [`std::collections::HashSet`] to get a de-duplicated set of resources + /// ``` + /// use std::collections::HashSet; + /// use cedar_policy::Schema; + /// let schema : Schema = Schema::from_cedarschema_str(r#" + /// entity User; + /// entity Folder; + /// action Access appliesTo { + /// principal : User, + /// resource : Folder, + /// }; + /// action Delete appliesTo { + /// principal : User, + /// resource : Folder, + /// }; + /// "#).unwrap().0; + /// let resources = schema.resources().collect::>(); + /// assert_eq!(resources, HashSet::from([&"Folder".parse().unwrap()])); + /// ``` + pub fn resources(&self) -> impl Iterator { + self.0.resources().filter_map(|ty| match ty { + EntityType::Specified(name) => Some(EntityTypeName::ref_cast(name)), + EntityType::Unspecified => None, + }) + } + + /// Returns an iterator over every entity type that can be a principal for `action` in this schema + /// + /// # Errors + /// + /// Returns [`None`] if `action` is not found in the schema + pub fn principals_for_action( + &self, + action: &EntityUid, + ) -> Option> { + self.0.principals_for_action(&action.0).map(|iter| { + iter.filter_map(|ty| match ty { + EntityType::Specified(name) => Some(EntityTypeName::ref_cast(name)), + EntityType::Unspecified => None, + }) + }) + } + + /// Returns an iterator over every entity type that can be a resource for `action` in this schema + /// + /// # Errors + /// + /// Returns [`None`] if `action` is not found in the schema + pub fn resources_for_action( + &self, + action: &EntityUid, + ) -> Option> { + self.0.resources_for_action(&action.0).map(|iter| { + iter.filter_map(|ty| match ty { + EntityType::Specified(name) => Some(EntityTypeName::ref_cast(name)), + EntityType::Unspecified => None, + }) + }) + } + + /// Returns an iterator over all the entity types that can be an ancestor of `ty` + /// + /// # Errors + /// + /// Returns [`None`] if the `ty` is not found in the schema + pub fn ancestors<'a>( + &'a self, + ty: &'a EntityTypeName, + ) -> Option + 'a> { + self.0 + .ancestors(&ty.0) + .map(|iter| iter.map(RefCast::ref_cast)) + } + + /// Returns an iterator over all the action groups defined in this schema + pub fn action_groups(&self) -> impl Iterator { + self.0.action_groups().map(RefCast::ref_cast) + } + + /// Returns an iterator over all entity types defined in this schema + pub fn entity_types(&self) -> impl Iterator { + self.0 + .entity_types() + .map(|(name, _)| RefCast::ref_cast(name)) + } + + /// Returns an iterator over all actions defined in this schema + pub fn actions(&self) -> impl Iterator { + self.0.actions().map(RefCast::ref_cast) + } } /// Errors encountered during construction of a Validation Schema @@ -5589,3 +5715,432 @@ mod test { assert!(err.contains("while parsing a template link, expected a literal entity reference")); } } +// These are the same tests in validator, just ensuring all the plumbing is done correctly +#[cfg(test)] +mod test_access { + use super::*; + + fn schema() -> Schema { + let src = r#" + type Task = { + "id": Long, + "name": String, + "state": String, +}; + +type Tasks = Set; +entity List in [Application] = { + "editors": Team, + "name": String, + "owner": User, + "readers": Team, + "tasks": Tasks, +}; +entity Application; +entity User in [Team, Application] = { + "joblevel": Long, + "location": String, +}; + +entity CoolList; + +entity Team in [Team, Application]; + +action Read, Write, Create; + +action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo { + principal: [User], + resource : [List] +}; + +action GetList in Read appliesTo { + principal : [User], + resource : [List, CoolList] +}; + +action GetLists in Read appliesTo { + principal : [User], + resource : [Application] +}; + +action CreateList in Create appliesTo { + principal : [User], + resource : [Application] +}; + + "#; + + Schema::from_cedarschema_str(src).unwrap().0 + } + + #[test] + fn principals() { + let schema = schema(); + let principals = schema.principals().collect::>(); + assert_eq!(principals.len(), 1); + let user: EntityTypeName = "User".parse().unwrap(); + assert!(principals.contains(&user)); + let principals = schema.principals().collect::>(); + assert!(principals.len() > 1); + assert!(principals.iter().all(|ety| **ety == user)); + } + + #[test] + fn empty_schema_principals_and_resources() { + let empty: Schema = Schema::from_cedarschema_str("").unwrap().0; + assert!(empty.principals().collect::>().is_empty()); + assert!(empty.resources().collect::>().is_empty()); + } + + #[test] + fn resources() { + let schema = schema(); + let resources = schema.resources().cloned().collect::>(); + let expected: HashSet = HashSet::from([ + "List".parse().unwrap(), + "Application".parse().unwrap(), + "CoolList".parse().unwrap(), + ]); + assert_eq!(resources, expected); + } + + #[test] + fn principals_for_action() { + let schema = schema(); + let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap(); + let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap(); + let got = schema + .principals_for_action(&delete_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!(got, vec!["User".parse().unwrap()]); + assert!(schema.principals_for_action(&delete_user).is_none()); + } + + #[test] + fn resources_for_action() { + let schema = schema(); + let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap(); + let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap(); + let create_list: EntityUid = r#"Action::"CreateList""#.parse().unwrap(); + let get_list: EntityUid = r#"Action::"GetList""#.parse().unwrap(); + let got = schema + .resources_for_action(&delete_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!(got, vec!["List".parse().unwrap()]); + let got = schema + .resources_for_action(&create_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!(got, vec!["Application".parse().unwrap()]); + let got = schema + .resources_for_action(&get_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!( + got, + HashSet::from(["List".parse().unwrap(), "CoolList".parse().unwrap()]) + ); + assert!(schema.principals_for_action(&delete_user).is_none()); + } + + #[test] + fn principal_parents() { + let schema = schema(); + let user: EntityTypeName = "User".parse().unwrap(); + let parents = schema + .ancestors(&user) + .unwrap() + .cloned() + .collect::>(); + let expected = HashSet::from(["Team".parse().unwrap(), "Application".parse().unwrap()]); + assert_eq!(parents, expected); + let parents = schema + .ancestors(&"List".parse().unwrap()) + .unwrap() + .cloned() + .collect::>(); + let expected = HashSet::from(["Application".parse().unwrap()]); + assert_eq!(parents, expected); + assert!(schema.ancestors(&"Foo".parse().unwrap()).is_none()); + let parents = schema + .ancestors(&"CoolList".parse().unwrap()) + .unwrap() + .cloned() + .collect::>(); + let expected = HashSet::from([]); + assert_eq!(parents, expected); + } + + #[test] + fn action_groups() { + let schema = schema(); + let groups = schema.action_groups().cloned().collect::>(); + let expected = ["Read", "Write", "Create"] + .into_iter() + .map(|ty| format!("Action::\"{ty}\"").parse().unwrap()) + .collect::>(); + assert_eq!(groups, expected); + } + + #[test] + fn actions() { + let schema = schema(); + let actions = schema.actions().cloned().collect::>(); + let expected = [ + "Read", + "Write", + "Create", + "DeleteList", + "EditShare", + "UpdateList", + "CreateTask", + "UpdateTask", + "DeleteTask", + "GetList", + "GetLists", + "CreateList", + ] + .into_iter() + .map(|ty| format!("Action::\"{ty}\"").parse().unwrap()) + .collect::>(); + assert_eq!(actions, expected); + } + + #[test] + fn entities() { + let schema = schema(); + let entities = schema.entity_types().cloned().collect::>(); + let expected = ["List", "Application", "User", "CoolList", "Team"] + .into_iter() + .map(|ty| ty.parse().unwrap()) + .collect::>(); + assert_eq!(entities, expected); + } +} + +#[cfg(test)] +mod test_access_namespace { + use super::*; + + fn schema() -> Schema { + let src = r#" + namespace Foo { + type Task = { + "id": Long, + "name": String, + "state": String, +}; + +type Tasks = Set; +entity List in [Application] = { + "editors": Team, + "name": String, + "owner": User, + "readers": Team, + "tasks": Tasks, +}; +entity Application; +entity User in [Team, Application] = { + "joblevel": Long, + "location": String, +}; + +entity CoolList; + +entity Team in [Team, Application]; + +action Read, Write, Create; + +action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo { + principal: [User], + resource : [List] +}; + +action GetList in Read appliesTo { + principal : [User], + resource : [List, CoolList] +}; + +action GetLists in Read appliesTo { + principal : [User], + resource : [Application] +}; + +action CreateList in Create appliesTo { + principal : [User], + resource : [Application] +}; + } + + "#; + Schema::from_cedarschema_str(src).unwrap().0 + } + + #[test] + fn principals() { + let schema = schema(); + let principals = schema.principals().collect::>(); + assert_eq!(principals.len(), 1); + let user: EntityTypeName = "Foo::User".parse().unwrap(); + assert!(principals.contains(&user)); + let principals = schema.principals().collect::>(); + assert!(principals.len() > 1); + assert!(principals.iter().all(|ety| **ety == user)); + } + + #[test] + fn empty_schema_principals_and_resources() { + let empty: Schema = Schema::from_cedarschema_str("").unwrap().0; + assert!(empty.principals().collect::>().is_empty()); + assert!(empty.resources().collect::>().is_empty()); + } + + #[test] + fn resources() { + let schema = schema(); + let resources = schema.resources().cloned().collect::>(); + let expected: HashSet = HashSet::from([ + "Foo::List".parse().unwrap(), + "Foo::Application".parse().unwrap(), + "Foo::CoolList".parse().unwrap(), + ]); + assert_eq!(resources, expected); + } + + #[test] + fn principals_for_action() { + let schema = schema(); + let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap(); + let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap(); + let got = schema + .principals_for_action(&delete_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!(got, vec!["Foo::User".parse().unwrap()]); + assert!(schema.principals_for_action(&delete_user).is_none()); + } + + #[test] + fn resources_for_action() { + let schema = schema(); + let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap(); + let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap(); + let create_list: EntityUid = r#"Foo::Action::"CreateList""#.parse().unwrap(); + let get_list: EntityUid = r#"Foo::Action::"GetList""#.parse().unwrap(); + let got = schema + .resources_for_action(&delete_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!(got, vec!["Foo::List".parse().unwrap()]); + let got = schema + .resources_for_action(&create_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!(got, vec!["Foo::Application".parse().unwrap()]); + let got = schema + .resources_for_action(&get_list) + .unwrap() + .cloned() + .collect::>(); + assert_eq!( + got, + HashSet::from([ + "Foo::List".parse().unwrap(), + "Foo::CoolList".parse().unwrap() + ]) + ); + assert!(schema.principals_for_action(&delete_user).is_none()); + } + + #[test] + fn principal_parents() { + let schema = schema(); + let user: EntityTypeName = "Foo::User".parse().unwrap(); + let parents = schema + .ancestors(&user) + .unwrap() + .cloned() + .collect::>(); + let expected = HashSet::from([ + "Foo::Team".parse().unwrap(), + "Foo::Application".parse().unwrap(), + ]); + assert_eq!(parents, expected); + let parents = schema + .ancestors(&"Foo::List".parse().unwrap()) + .unwrap() + .cloned() + .collect::>(); + let expected = HashSet::from(["Foo::Application".parse().unwrap()]); + assert_eq!(parents, expected); + assert!(schema.ancestors(&"Foo::Foo".parse().unwrap()).is_none()); + let parents = schema + .ancestors(&"Foo::CoolList".parse().unwrap()) + .unwrap() + .cloned() + .collect::>(); + let expected = HashSet::from([]); + assert_eq!(parents, expected); + } + + #[test] + fn action_groups() { + let schema = schema(); + let groups = schema.action_groups().cloned().collect::>(); + let expected = ["Read", "Write", "Create"] + .into_iter() + .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap()) + .collect::>(); + assert_eq!(groups, expected); + } + + #[test] + fn actions() { + let schema = schema(); + let actions = schema.actions().cloned().collect::>(); + let expected = [ + "Read", + "Write", + "Create", + "DeleteList", + "EditShare", + "UpdateList", + "CreateTask", + "UpdateTask", + "DeleteTask", + "GetList", + "GetLists", + "CreateList", + ] + .into_iter() + .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap()) + .collect::>(); + assert_eq!(actions, expected); + } + + #[test] + fn entities() { + let schema = schema(); + let entities = schema.entity_types().cloned().collect::>(); + let expected = [ + "Foo::List", + "Foo::Application", + "Foo::User", + "Foo::CoolList", + "Foo::Team", + ] + .into_iter() + .map(|ty| ty.parse().unwrap()) + .collect::>(); + assert_eq!(entities, expected); + } +}