use std::collections::{hash_map::Entry, BTreeMap, HashMap, HashSet};
use cedar_policy_core::{
ast::{Entity, EntityType, EntityUID, Name},
entities::{Entities, EntitiesError, TCComputation},
extensions::Extensions,
transitive_closure::compute_tc,
};
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use super::NamespaceDefinition;
use crate::{
err::*,
human_schema::SchemaWarning,
types::{Attributes, EntityRecordKind, OpenTag, Type},
SchemaFragment, SchemaType, SchemaTypeVariant, TypeOfAttribute,
};
mod action;
pub use action::ValidatorActionId;
pub(crate) use action::ValidatorApplySpec;
mod entity_type;
pub use entity_type::ValidatorEntityType;
mod namespace_def;
pub(crate) use namespace_def::is_action_entity_type;
pub use namespace_def::ValidatorNamespaceDef;
#[cfg(test)]
pub(crate) use namespace_def::ACTION_ENTITY_TYPE;
#[derive(Eq, PartialEq, Copy, Clone, Default)]
pub enum ActionBehavior {
#[default]
ProhibitAttributes,
PermitAttributes,
}
#[derive(Debug)]
pub struct ValidatorSchemaFragment(Vec<ValidatorNamespaceDef>);
impl TryInto<ValidatorSchemaFragment> for SchemaFragment {
type Error = SchemaError;
fn try_into(self) -> Result<ValidatorSchemaFragment> {
ValidatorSchemaFragment::from_schema_fragment(
self,
ActionBehavior::default(),
Extensions::all_available(),
)
}
}
impl ValidatorSchemaFragment {
pub fn from_namespaces(namespaces: impl IntoIterator<Item = ValidatorNamespaceDef>) -> Self {
Self(namespaces.into_iter().collect())
}
pub fn from_schema_fragment(
fragment: SchemaFragment,
action_behavior: ActionBehavior,
extensions: Extensions<'_>,
) -> Result<Self> {
Ok(Self(
fragment
.0
.into_iter()
.map(|(fragment_ns, ns_def)| {
ValidatorNamespaceDef::from_namespace_definition(
fragment_ns,
ns_def,
action_behavior,
extensions,
)
})
.collect::<Result<Vec<_>>>()?,
))
}
pub fn namespaces(&self) -> impl Iterator<Item = &Option<Name>> {
self.0.iter().map(|d| d.namespace())
}
}
#[serde_as]
#[derive(Clone, Debug, Serialize)]
pub struct ValidatorSchema {
#[serde(rename = "entityTypes")]
#[serde_as(as = "Vec<(_, _)>")]
entity_types: HashMap<Name, ValidatorEntityType>,
#[serde(rename = "actionIds")]
#[serde_as(as = "Vec<(_, _)>")]
action_ids: HashMap<EntityUID, ValidatorActionId>,
}
impl std::str::FromStr for ValidatorSchema {
type Err = SchemaError;
fn from_str(s: &str) -> Result<Self> {
serde_json::from_str::<SchemaFragment>(s)?.try_into()
}
}
impl TryFrom<NamespaceDefinition> for ValidatorSchema {
type Error = SchemaError;
fn try_from(nsd: NamespaceDefinition) -> Result<ValidatorSchema> {
ValidatorSchema::from_schema_fragments([ValidatorSchemaFragment::from_namespaces([
nsd.try_into()?
])])
}
}
impl TryFrom<SchemaFragment> for ValidatorSchema {
type Error = SchemaError;
fn try_from(frag: SchemaFragment) -> Result<ValidatorSchema> {
ValidatorSchema::from_schema_fragments([frag.try_into()?])
}
}
impl ValidatorSchema {
pub fn principals(&self) -> impl Iterator<Item = &EntityType> {
self.action_ids
.values()
.flat_map(ValidatorActionId::principals)
}
pub fn resources(&self) -> impl Iterator<Item = &EntityType> {
self.action_ids
.values()
.flat_map(ValidatorActionId::resources)
}
pub fn principals_for_action(
&self,
action: &EntityUID,
) -> Option<impl Iterator<Item = &EntityType>> {
self.action_ids
.get(action)
.map(ValidatorActionId::principals)
}
pub fn resources_for_action(
&self,
action: &EntityUID,
) -> Option<impl Iterator<Item = &EntityType>> {
self.action_ids
.get(action)
.map(ValidatorActionId::resources)
}
pub fn ancestors<'a>(&'a self, ty: &'a Name) -> Option<impl Iterator<Item = &Name> + '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
}
}
pub fn action_groups(&self) -> impl Iterator<Item = &EntityUID> {
self.action_ids.values().filter_map(|action| {
if action.descendants.is_empty() {
None
} else {
Some(&action.name)
}
})
}
pub fn actions(&self) -> impl Iterator<Item = &EntityUID> {
self.action_ids.keys()
}
pub fn empty() -> ValidatorSchema {
Self {
entity_types: HashMap::new(),
action_ids: HashMap::new(),
}
}
pub fn from_json_value(json: serde_json::Value, extensions: Extensions<'_>) -> Result<Self> {
Self::from_schema_file(
SchemaFragment::from_json_value(json)?,
ActionBehavior::default(),
extensions,
)
}
pub fn from_file(file: impl std::io::Read, extensions: Extensions<'_>) -> Result<Self> {
Self::from_schema_file(
SchemaFragment::from_file(file)?,
ActionBehavior::default(),
extensions,
)
}
pub fn from_file_natural(
r: impl std::io::Read,
extensions: Extensions<'_>,
) -> std::result::Result<(Self, impl Iterator<Item = SchemaWarning>), HumanSchemaError> {
let (fragment, warnings) = SchemaFragment::from_file_natural(r)?;
let schema_and_warnings =
Self::from_schema_file(fragment, ActionBehavior::default(), extensions)
.map(|schema| (schema, warnings))?;
Ok(schema_and_warnings)
}
pub fn from_str_natural(
src: &str,
extensions: Extensions<'_>,
) -> std::result::Result<(Self, impl Iterator<Item = SchemaWarning>), HumanSchemaError> {
let (fragment, warnings) = SchemaFragment::from_str_natural(src)?;
let schema_and_warnings =
Self::from_schema_file(fragment, ActionBehavior::default(), extensions)
.map(|schema| (schema, warnings))?;
Ok(schema_and_warnings)
}
pub fn from_schema_file(
schema_file: SchemaFragment,
action_behavior: ActionBehavior,
extensions: Extensions<'_>,
) -> Result<ValidatorSchema> {
Self::from_schema_fragments([ValidatorSchemaFragment::from_schema_fragment(
schema_file,
action_behavior,
extensions,
)?])
}
pub fn from_schema_fragments(
fragments: impl IntoIterator<Item = ValidatorSchemaFragment>,
) -> Result<ValidatorSchema> {
let mut type_defs = HashMap::new();
let mut entity_type_fragments = HashMap::new();
let mut action_fragments = HashMap::new();
for ns_def in fragments.into_iter().flat_map(|f| f.0.into_iter()) {
for (name, ty) in ns_def.type_defs.type_defs {
match type_defs.entry(name) {
Entry::Vacant(v) => v.insert(ty),
Entry::Occupied(o) => {
return Err(SchemaError::DuplicateCommonType(o.key().to_string()));
}
};
}
for (name, entity_type) in ns_def.entity_types.entity_types {
match entity_type_fragments.entry(name) {
Entry::Vacant(v) => v.insert(entity_type),
Entry::Occupied(o) => {
return Err(SchemaError::DuplicateEntityType(o.key().to_string()))
}
};
}
for (action_euid, action) in ns_def.actions.actions {
match action_fragments.entry(action_euid) {
Entry::Vacant(v) => v.insert(action),
Entry::Occupied(o) => {
return Err(SchemaError::DuplicateAction(o.key().to_string()))
}
};
}
}
let resolver = CommonTypeResolver::new(&type_defs);
let type_defs = resolver.resolve()?;
let mut entity_children = HashMap::new();
for (name, entity_type) in entity_type_fragments.iter() {
for parent in entity_type.parents.iter() {
entity_children
.entry(parent.clone())
.or_insert_with(HashSet::new)
.insert(name.clone());
}
}
let mut entity_types = entity_type_fragments
.into_iter()
.map(|(name, entity_type)| -> Result<_> {
let descendants = entity_children.remove(&name).unwrap_or_default();
let (attributes, open_attributes) = Self::record_attributes_or_none(
entity_type.attributes.resolve_type_defs(&type_defs)?,
)
.ok_or(SchemaError::ContextOrShapeNotRecord(
ContextOrShape::EntityTypeShape(name.clone()),
))?;
Ok((
name.clone(),
ValidatorEntityType {
name,
descendants,
attributes,
open_attributes,
},
))
})
.collect::<Result<HashMap<_, _>>>()?;
let mut action_children = HashMap::new();
for (euid, action) in action_fragments.iter() {
for parent in action.parents.iter() {
action_children
.entry(parent.clone())
.or_insert_with(HashSet::new)
.insert(euid.clone());
}
}
let mut action_ids = action_fragments
.into_iter()
.map(|(name, action)| -> Result<_> {
let descendants = action_children.remove(&name).unwrap_or_default();
let (context, open_context_attributes) =
Self::record_attributes_or_none(action.context.resolve_type_defs(&type_defs)?)
.ok_or(SchemaError::ContextOrShapeNotRecord(
ContextOrShape::ActionContext(name.clone()),
))?;
Ok((
name.clone(),
ValidatorActionId {
name,
applies_to: action.applies_to,
descendants,
context: Type::record_with_attributes(
context.attrs,
open_context_attributes,
),
attribute_types: action.attribute_types,
attributes: action.attributes,
},
))
})
.collect::<Result<HashMap<_, _>>>()?;
compute_tc(&mut entity_types, false)?;
compute_tc(&mut action_ids, true)?;
Self::check_for_undeclared(
&entity_types,
entity_children.into_keys(),
&action_ids,
action_children.into_keys(),
)?;
Ok(ValidatorSchema {
entity_types,
action_ids,
})
}
fn check_for_undeclared(
entity_types: &HashMap<Name, ValidatorEntityType>,
undeclared_parent_entities: impl IntoIterator<Item = Name>,
action_ids: &HashMap<EntityUID, ValidatorActionId>,
undeclared_parent_actions: impl IntoIterator<Item = EntityUID>,
) -> Result<()> {
let mut undeclared_e = undeclared_parent_entities
.into_iter()
.map(|n| n.to_string())
.collect::<HashSet<_>>();
for entity_type in entity_types.values() {
for (_, attr_typ) in entity_type.attributes() {
Self::check_undeclared_in_type(
&attr_typ.attr_type,
entity_types,
&mut undeclared_e,
);
}
}
let undeclared_a = undeclared_parent_actions
.into_iter()
.map(|n| n.to_string())
.collect::<HashSet<_>>();
for action in action_ids.values() {
Self::check_undeclared_in_type(&action.context, entity_types, &mut undeclared_e);
for p_entity in action.applies_to.applicable_principal_types() {
match p_entity {
EntityType::Specified(p_entity) => {
if !entity_types.contains_key(&p_entity) {
undeclared_e.insert(p_entity.to_string());
}
}
EntityType::Unspecified => (),
}
}
for r_entity in action.applies_to.applicable_resource_types() {
match r_entity {
EntityType::Specified(r_entity) => {
if !entity_types.contains_key(&r_entity) {
undeclared_e.insert(r_entity.to_string());
}
}
EntityType::Unspecified => (),
}
}
}
if !undeclared_e.is_empty() {
return Err(SchemaError::UndeclaredEntityTypes(undeclared_e));
}
if !undeclared_a.is_empty() {
return Err(SchemaError::UndeclaredActions(undeclared_a));
}
Ok(())
}
fn record_attributes_or_none(ty: Type) -> Option<(Attributes, OpenTag)> {
match ty {
Type::EntityOrRecord(EntityRecordKind::Record {
attrs,
open_attributes,
}) => Some((attrs, open_attributes)),
_ => None,
}
}
fn check_undeclared_in_type(
ty: &Type,
entity_types: &HashMap<Name, ValidatorEntityType>,
undeclared_types: &mut HashSet<String>,
) {
match ty {
Type::EntityOrRecord(EntityRecordKind::Entity(lub)) => {
for name in lub.iter() {
if !entity_types.contains_key(name) {
undeclared_types.insert(name.to_string());
}
}
}
Type::EntityOrRecord(EntityRecordKind::Record { attrs, .. }) => {
for (_, attr_ty) in attrs.iter() {
Self::check_undeclared_in_type(
&attr_ty.attr_type,
entity_types,
undeclared_types,
);
}
}
Type::Set {
element_type: Some(element_type),
} => Self::check_undeclared_in_type(element_type, entity_types, undeclared_types),
_ => (),
}
}
pub fn get_action_id(&self, action_id: &EntityUID) -> Option<&ValidatorActionId> {
self.action_ids.get(action_id)
}
pub fn get_entity_type<'a>(&'a self, entity_type_id: &Name) -> Option<&'a ValidatorEntityType> {
self.entity_types.get(entity_type_id)
}
pub(crate) fn is_known_action_id(&self, action_id: &EntityUID) -> bool {
self.action_ids.contains_key(action_id)
}
pub(crate) fn is_known_entity_type(&self, entity_type: &Name) -> bool {
is_action_entity_type(entity_type) || self.entity_types.contains_key(entity_type)
}
pub(crate) fn euid_has_known_entity_type(&self, euid: &EntityUID) -> bool {
match euid.entity_type() {
EntityType::Specified(ety) => self.is_known_entity_type(ety),
EntityType::Unspecified => true,
}
}
pub(crate) fn known_action_ids(&self) -> impl Iterator<Item = &EntityUID> {
self.action_ids.keys()
}
pub(crate) fn known_entity_types(&self) -> impl Iterator<Item = &Name> {
self.entity_types.keys()
}
pub fn entity_types(&self) -> impl Iterator<Item = (&Name, &ValidatorEntityType)> {
self.entity_types.iter()
}
pub(crate) fn get_entity_types_in<'a>(&'a self, entity: &'a EntityUID) -> Vec<&Name> {
match entity.entity_type() {
EntityType::Specified(ety) => {
let mut descendants = self
.get_entity_type(ety)
.map(|v_ety| v_ety.descendants.iter().collect::<Vec<_>>())
.unwrap_or_default();
descendants.push(ety);
descendants
}
EntityType::Unspecified => Vec::new(),
}
}
pub(crate) fn get_entity_types_in_set<'a>(
&'a self,
euids: impl IntoIterator<Item = &'a EntityUID> + 'a,
) -> impl Iterator<Item = &Name> {
euids.into_iter().flat_map(|e| self.get_entity_types_in(e))
}
pub(crate) fn get_actions_in_set<'a>(
&'a self,
euids: impl IntoIterator<Item = &'a EntityUID> + 'a,
) -> Option<Vec<&'a EntityUID>> {
euids
.into_iter()
.map(|e| {
self.get_action_id(e).map(|action| {
action
.descendants
.iter()
.chain(std::iter::once(&action.name))
})
})
.collect::<Option<Vec<_>>>()
.map(|v| v.into_iter().flatten().collect::<Vec<_>>())
}
pub fn context_type(&self, action: &EntityUID) -> Option<Type> {
self.get_action_id(action)
.map(ValidatorActionId::context_type)
}
pub(crate) fn action_entities_iter(
&self,
) -> impl Iterator<Item = cedar_policy_core::ast::Entity> + '_ {
let mut action_ancestors: HashMap<&EntityUID, HashSet<EntityUID>> = HashMap::new();
for (action_euid, action_def) in &self.action_ids {
for descendant in &action_def.descendants {
action_ancestors
.entry(descendant)
.or_default()
.insert(action_euid.clone());
}
}
self.action_ids.iter().map(move |(action_id, action)| {
Entity::new_with_attr_partial_value_serialized_as_expr(
action_id.clone(),
action.attributes.clone(),
action_ancestors.remove(action_id).unwrap_or_default(),
)
})
}
pub fn action_entities(&self) -> std::result::Result<Entities, EntitiesError> {
let extensions = Extensions::all_available();
Entities::from_entities(
self.action_entities_iter(),
None::<&cedar_policy_core::entities::NoEntitiesSchema>, TCComputation::AssumeAlreadyComputed,
extensions,
)
.map_err(Into::into)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(transparent)]
pub(crate) struct NamespaceDefinitionWithActionAttributes(pub(crate) NamespaceDefinition);
impl TryInto<ValidatorSchema> for NamespaceDefinitionWithActionAttributes {
type Error = SchemaError;
fn try_into(self) -> Result<ValidatorSchema> {
ValidatorSchema::from_schema_fragments([ValidatorSchemaFragment::from_namespaces([
ValidatorNamespaceDef::from_namespace_definition(
None,
self.0,
crate::ActionBehavior::PermitAttributes,
Extensions::all_available(),
)?,
])])
}
}
#[derive(Debug)]
struct CommonTypeResolver<'a> {
type_defs: &'a HashMap<Name, SchemaType>,
graph: HashMap<Name, HashSet<Name>>,
}
impl<'a> CommonTypeResolver<'a> {
fn new(type_defs: &'a HashMap<Name, SchemaType>) -> Self {
let mut graph = HashMap::new();
for (name, ty) in type_defs {
graph.insert(
name.clone(),
HashSet::from_iter(ty.common_type_references()),
);
}
Self { type_defs, graph }
}
fn topo_sort(&self) -> std::result::Result<Vec<Name>, Name> {
let mut indegrees: HashMap<&Name, usize> = HashMap::new();
for (ty_name, deps) in self.graph.iter() {
indegrees.entry(ty_name).or_insert(0);
for dep in deps {
match indegrees.entry(dep) {
std::collections::hash_map::Entry::Occupied(mut o) => {
o.insert(o.get() + 1);
}
std::collections::hash_map::Entry::Vacant(v) => {
v.insert(1);
}
}
}
}
let mut work_set: HashSet<&Name> = HashSet::new();
let mut res: Vec<Name> = Vec::new();
for (name, degree) in indegrees.iter() {
let name = *name;
if *degree == 0 {
work_set.insert(name);
if self.graph.contains_key(name) {
res.push(name.clone());
}
}
}
while let Some(name) = work_set.iter().next().cloned() {
work_set.remove(name);
if let Some(deps) = self.graph.get(name) {
for dep in deps {
if let Some(degree) = indegrees.get_mut(dep) {
*degree -= 1;
if *degree == 0 {
work_set.insert(dep);
if self.graph.contains_key(dep) {
res.push(dep.clone());
}
}
}
}
}
}
let mut set: HashSet<&Name> = HashSet::from_iter(self.graph.keys().clone());
for name in res.iter() {
set.remove(name);
}
if let Some(cycle) = set.into_iter().next() {
Err(cycle.clone())
} else {
res.reverse();
Ok(res)
}
}
fn resolve_type(
resolve_table: &HashMap<&Name, SchemaType>,
ty: SchemaType,
) -> Result<SchemaType> {
match ty {
SchemaType::TypeDef { type_name } => resolve_table
.get(&type_name)
.ok_or(SchemaError::UndeclaredCommonTypes(HashSet::from_iter(
std::iter::once(type_name.to_string()),
)))
.cloned(),
SchemaType::Type(SchemaTypeVariant::Set { element }) => {
Ok(SchemaType::Type(SchemaTypeVariant::Set {
element: Box::new(Self::resolve_type(resolve_table, *element)?),
}))
}
SchemaType::Type(SchemaTypeVariant::Record {
attributes,
additional_attributes,
}) => Ok(SchemaType::Type(SchemaTypeVariant::Record {
attributes: BTreeMap::from_iter(
attributes
.into_iter()
.map(|(attr, attr_ty)| {
Ok((
attr,
TypeOfAttribute {
required: attr_ty.required,
ty: Self::resolve_type(resolve_table, attr_ty.ty)?,
},
))
})
.collect::<Result<Vec<(_, _)>>>()?,
),
additional_attributes,
})),
_ => Ok(ty),
}
}
fn resolve(&self) -> Result<HashMap<Name, Type>> {
let sorted_names = self
.topo_sort()
.map_err(SchemaError::CycleInCommonTypeReferences)?;
let mut resolve_table = HashMap::new();
let mut tys = HashMap::new();
for name in sorted_names.iter() {
let ns: Option<Name> = if name.is_unqualified() {
None
} else {
#[allow(clippy::unwrap_used)]
Some(name.namespace().parse().unwrap())
};
#[allow(clippy::unwrap_used)]
let ty = self.type_defs.get(name).unwrap();
let substituted_ty = Self::resolve_type(&resolve_table, ty.clone())?;
resolve_table.insert(name, substituted_ty.clone());
tys.insert(
name.clone(),
ValidatorNamespaceDef::try_schema_type_into_validator_type(
ns.as_ref(),
substituted_ty,
)?
.resolve_type_defs(&HashMap::new())?,
);
}
Ok(tys)
}
}
#[allow(clippy::panic)]
#[allow(clippy::indexing_slicing)]
#[cfg(test)]
mod test {
use std::{collections::BTreeMap, str::FromStr};
use crate::types::Type;
use crate::{SchemaType, SchemaTypeVariant};
use cedar_policy_core::ast::RestrictedExpr;
use cool_asserts::assert_matches;
use serde_json::json;
use super::*;
#[test]
fn test_from_schema_file() {
let src = json!(
{
"entityTypes": {
"User": {
"memberOfTypes": [ "Group" ]
},
"Group": {
"memberOfTypes": []
},
"Photo": {
"memberOfTypes": [ "Album" ]
},
"Album": {
"memberOfTypes": []
}
},
"actions": {
"view_photo": {
"appliesTo": {
"principalTypes": ["User", "Group"],
"resourceTypes": ["Photo"]
}
}
}
});
let schema_file: NamespaceDefinition = serde_json::from_value(src).expect("Parse Error");
let schema: Result<ValidatorSchema> = schema_file.try_into();
assert!(schema.is_ok());
}
#[test]
fn test_from_schema_file_duplicate_entity() {
let src = r#"
{"": {
"entityTypes": {
"User": {
"memberOfTypes": [ "Group" ]
},
"Group": {
"memberOfTypes": []
},
"Photo": {
"memberOfTypes": [ "Album" ]
},
"Photo": {
"memberOfTypes": []
}
},
"actions": {
"view_photo": {
"memberOf": [],
"appliesTo": {
"principalTypes": ["User", "Group"],
"resourceTypes": ["Photo"]
}
}
}
}}"#;
match ValidatorSchema::from_str(src) {
Err(SchemaError::Serde(_)) => (),
_ => panic!("Expected serde error due to duplicate entity type."),
}
}
#[test]
fn test_from_schema_file_duplicate_action() {
let src = r#"
{"": {
"entityTypes": {
"User": {
"memberOfTypes": [ "Group" ]
},
"Group": {
"memberOfTypes": []
},
"Photo": {
"memberOfTypes": []
}
},
"actions": {
"view_photo": {
"memberOf": [],
"appliesTo": {
"principalTypes": ["User", "Group"],
"resourceTypes": ["Photo"]
}
},
"view_photo": { }
}
}"#;
match ValidatorSchema::from_str(src) {
Err(SchemaError::Serde(_)) => (),
_ => panic!("Expected serde error due to duplicate action type."),
}
}
#[test]
fn test_from_schema_file_undefined_entities() {
let src = json!(
{
"entityTypes": {
"User": {
"memberOfTypes": [ "Grop" ]
},
"Group": {
"memberOfTypes": []
},
"Photo": {
"memberOfTypes": []
}
},
"actions": {
"view_photo": {
"appliesTo": {
"principalTypes": ["Usr", "Group"],
"resourceTypes": ["Phoot"]
}
}
}
});
let schema_file: NamespaceDefinition = serde_json::from_value(src).expect("Parse Error");
let schema: Result<ValidatorSchema> = schema_file.try_into();
match schema {
Ok(_) => panic!("from_schema_file should have failed"),
Err(SchemaError::UndeclaredEntityTypes(v)) => {
assert_eq!(v.len(), 3)
}
_ => panic!("Unexpected error from from_schema_file"),
}
}
#[test]
fn undefined_entity_namespace_member_of() {
let src = json!(
{"Foo": {
"entityTypes": {
"User": {
"memberOfTypes": [ "Foo::Group", "Bar::Group" ]
},
"Group": { }
},
"actions": {}
}});
let schema_file: SchemaFragment = serde_json::from_value(src).expect("Parse Error");
let schema: Result<ValidatorSchema> = schema_file.try_into();
match schema {
Ok(_) => panic!("try_into should have failed"),
Err(SchemaError::UndeclaredEntityTypes(v)) => {
assert_eq!(v, HashSet::from(["Bar::Group".to_string()]))
}
_ => panic!("Unexpected error from try_into"),
}
}
#[test]
fn undefined_entity_namespace_applies_to() {
let src = json!(
{"Foo": {
"entityTypes": { "User": { }, "Photo": { } },
"actions": {
"view_photo": {
"appliesTo": {
"principalTypes": ["Foo::User", "Bar::User"],
"resourceTypes": ["Photo", "Bar::Photo"],
}
}
}
}});
let schema_file: SchemaFragment = serde_json::from_value(src).expect("Parse Error");
let schema: Result<ValidatorSchema> = schema_file.try_into();
match schema {
Ok(_) => panic!("try_into should have failed"),
Err(SchemaError::UndeclaredEntityTypes(v)) => {
assert_eq!(
v,
HashSet::from(["Bar::Photo".to_string(), "Bar::User".to_string()])
)
}
_ => panic!("Unexpected error from try_into"),
}
}
#[test]
fn test_from_schema_file_undefined_action() {
let src = json!(
{
"entityTypes": {
"User": {
"memberOfTypes": [ "Group" ]
},
"Group": {
"memberOfTypes": []
},
"Photo": {
"memberOfTypes": []
}
},
"actions": {
"view_photo": {
"memberOf": [ {"id": "photo_action"} ],
"appliesTo": {
"principalTypes": ["User", "Group"],
"resourceTypes": ["Photo"]
}
}
}
});
let schema_file: NamespaceDefinition = serde_json::from_value(src).expect("Parse Error");
let schema: Result<ValidatorSchema> = schema_file.try_into();
match schema {
Ok(_) => panic!("from_schema_file should have failed"),
Err(SchemaError::UndeclaredActions(v)) => assert_eq!(v.len(), 1),
_ => panic!("Unexpected error from from_schema_file"),
}
}
#[test]
fn test_from_schema_file_action_cycle1() {
let src = json!(
{
"entityTypes": {},
"actions": {
"view_photo": {
"memberOf": [ {"id": "view_photo"} ]
}
}
});
let schema_file: NamespaceDefinition = serde_json::from_value(src).expect("Parse Error");
let schema: Result<ValidatorSchema> = schema_file.try_into();
assert_matches!(
schema,
Err(SchemaError::CycleInActionHierarchy(euid)) => {
assert_eq!(euid, r#"Action::"view_photo""#.parse().unwrap());
}
)
}
#[test]
fn test_from_schema_file_action_cycle2() {
let src = json!(
{
"entityTypes": {},
"actions": {
"view_photo": {
"memberOf": [ {"id": "edit_photo"} ]
},
"edit_photo": {
"memberOf": [ {"id": "delete_photo"} ]
},
"delete_photo": {
"memberOf": [ {"id": "view_photo"} ]
},
"other_action": {
"memberOf": [ {"id": "edit_photo"} ]
}
}
});
let schema_file: NamespaceDefinition = serde_json::from_value(src).expect("Parse Error");
let schema: Result<ValidatorSchema> = schema_file.try_into();
assert_matches!(
schema,
Err(SchemaError::CycleInActionHierarchy(_)),
)
}
#[test]
fn namespaced_schema() {
let src = r#"
{ "N::S": {
"entityTypes": {
"User": {},
"Photo": {}
},
"actions": {
"view_photo": {
"appliesTo": {
"principalTypes": ["User"],
"resourceTypes": ["Photo"]
}
}
}
} }
"#;
let schema_file: SchemaFragment = serde_json::from_str(src).expect("Parse Error");
let schema: ValidatorSchema = schema_file
.try_into()
.expect("Namespaced schema failed to convert.");
dbg!(&schema);
let user_entity_type = &"N::S::User"
.parse()
.expect("Namespaced entity type should have parsed");
let photo_entity_type = &"N::S::Photo"
.parse()
.expect("Namespaced entity type should have parsed");
assert!(
schema.entity_types.contains_key(user_entity_type),
"Expected and entity type User."
);
assert!(
schema.entity_types.contains_key(photo_entity_type),
"Expected an entity type Photo."
);
assert_eq!(
schema.entity_types.len(),
2,
"Expected exactly 2 entity types."
);
assert!(
schema.action_ids.contains_key(
&"N::S::Action::\"view_photo\""
.parse()
.expect("Namespaced action should have parsed")
),
"Expected an action \"view_photo\"."
);
assert_eq!(schema.action_ids.len(), 1, "Expected exactly 1 action.");
let apply_spec = &schema
.action_ids
.values()
.next()
.expect("Expected Action")
.applies_to;
assert_eq!(
apply_spec.applicable_principal_types().collect::<Vec<_>>(),
vec![&EntityType::Specified(user_entity_type.clone())]
);
assert_eq!(
apply_spec.applicable_resource_types().collect::<Vec<_>>(),
vec![&EntityType::Specified(photo_entity_type.clone())]
);
}
#[test]
fn cant_use_namespace_in_entity_type() {
let src = r#"
{
"entityTypes": { "NS::User": {} },
"actions": {}
}
"#;
let schema_file: std::result::Result<NamespaceDefinition, _> = serde_json::from_str(src);
assert!(schema_file.is_err());
}
#[test]
fn entity_attribute_entity_type_with_namespace() {
let schema_json: SchemaFragment = serde_json::from_str(
r#"
{"A::B": {
"entityTypes": {
"Foo": {
"shape": {
"type": "Record",
"attributes": {
"name": { "type": "Entity", "name": "C::D::Foo" }
}
}
}
},
"actions": {}
}}
"#,
)
.expect("Expected valid schema");
let schema: Result<ValidatorSchema> = schema_json.try_into();
match schema {
Err(SchemaError::UndeclaredEntityTypes(tys)) => {
assert_eq!(tys, HashSet::from(["C::D::Foo".to_string()]))
}
_ => panic!("Schema construction should have failed due to undeclared entity type."),
}
}
#[test]
fn entity_attribute_entity_type_with_declared_namespace() {
let schema_json: SchemaFragment = serde_json::from_str(
r#"
{"A::B": {
"entityTypes": {
"Foo": {
"shape": {
"type": "Record",
"attributes": {
"name": { "type": "Entity", "name": "A::B::Foo" }
}
}
}
},
"actions": {}
}}
"#,
)
.expect("Expected valid schema");
let schema: ValidatorSchema = schema_json
.try_into()
.expect("Expected schema to construct without error.");
let foo_name: Name = "A::B::Foo".parse().expect("Expected entity type name");
let foo_type = schema
.entity_types
.get(&foo_name)
.expect("Expected to find entity");
let name_type = foo_type
.attr("name")
.expect("Expected attribute name")
.attr_type
.clone();
let expected_name_type = Type::named_entity_reference(foo_name);
assert_eq!(name_type, expected_name_type);
}
#[test]
fn cannot_declare_action_type_when_prohibited() {
let schema_json: NamespaceDefinition = serde_json::from_str(
r#"
{
"entityTypes": { "Action": {} },
"actions": {}
}
"#,
)
.expect("Expected valid schema");
let schema: Result<ValidatorSchema> = schema_json.try_into();
assert!(matches!(schema, Err(SchemaError::ActionEntityTypeDeclared)));
}
#[test]
fn can_declare_other_type_when_action_type_prohibited() {
let schema_json: NamespaceDefinition = serde_json::from_str(
r#"
{
"entityTypes": { "Foo": { } },
"actions": {}
}
"#,
)
.expect("Expected valid schema");
TryInto::<ValidatorSchema>::try_into(schema_json).expect("Did not expect any errors.");
}
#[test]
fn cannot_declare_action_in_group_when_prohibited() {
let schema_json: SchemaFragment = serde_json::from_str(
r#"
{"": {
"entityTypes": {},
"actions": {
"universe": { },
"view_photo": {
"attributes": {"id": "universe"}
},
"edit_photo": {
"attributes": {"id": "universe"}
},
"delete_photo": {
"attributes": {"id": "universe"}
}
}
}}
"#,
)
.expect("Expected valid schema");
let schema = ValidatorSchemaFragment::from_schema_fragment(
schema_json,
ActionBehavior::ProhibitAttributes,
Extensions::all_available(),
);
match schema {
Err(SchemaError::UnsupportedFeature(UnsupportedFeature::ActionAttributes(actions))) => {
assert_eq!(
actions.into_iter().collect::<HashSet<_>>(),
HashSet::from([
"view_photo".to_string(),
"edit_photo".to_string(),
"delete_photo".to_string(),
])
)
}
_ => panic!("Did not see expected error."),
}
}
#[test]
fn test_entity_type_no_namespace() {
let src = json!({"type": "Entity", "name": "Foo"});
let schema_ty: SchemaType = serde_json::from_value(src).expect("Parse Error");
assert_eq!(
schema_ty,
SchemaType::Type(SchemaTypeVariant::Entity {
name: "Foo".parse().unwrap()
})
);
let ty: Type = ValidatorNamespaceDef::try_schema_type_into_validator_type(
Some(&Name::parse_unqualified_name("NS").expect("Expected namespace.")),
schema_ty,
)
.expect("Error converting schema type to type.")
.resolve_type_defs(&HashMap::new())
.unwrap();
assert_eq!(ty, Type::named_entity_reference_from_str("NS::Foo"));
}
#[test]
fn test_entity_type_namespace() {
let src = json!({"type": "Entity", "name": "NS::Foo"});
let schema_ty: SchemaType = serde_json::from_value(src).expect("Parse Error");
assert_eq!(
schema_ty,
SchemaType::Type(SchemaTypeVariant::Entity {
name: "NS::Foo".parse().unwrap()
})
);
let ty: Type = ValidatorNamespaceDef::try_schema_type_into_validator_type(
Some(&Name::parse_unqualified_name("NS").expect("Expected namespace.")),
schema_ty,
)
.expect("Error converting schema type to type.")
.resolve_type_defs(&HashMap::new())
.unwrap();
assert_eq!(ty, Type::named_entity_reference_from_str("NS::Foo"));
}
#[test]
fn test_entity_type_namespace_parse_error() {
let src = json!({"type": "Entity", "name": "::Foo"});
let schema_ty: std::result::Result<SchemaType, _> = serde_json::from_value(src);
assert!(schema_ty.is_err());
}
#[test]
fn schema_type_record_is_validator_type_record() {
let src = json!({"type": "Record", "attributes": {}});
let schema_ty: SchemaType = serde_json::from_value(src).expect("Parse Error");
assert_eq!(
schema_ty,
SchemaType::Type(SchemaTypeVariant::Record {
attributes: BTreeMap::new(),
additional_attributes: false,
}),
);
let ty: Type = ValidatorNamespaceDef::try_schema_type_into_validator_type(None, schema_ty)
.expect("Error converting schema type to type.")
.resolve_type_defs(&HashMap::new())
.unwrap();
assert_eq!(ty, Type::closed_record_with_attributes(None));
}
#[test]
fn get_namespaces() {
let fragment: SchemaFragment = serde_json::from_value(json!({
"Foo::Bar::Baz": {
"entityTypes": {},
"actions": {}
},
"Foo": {
"entityTypes": {},
"actions": {}
},
"Bar": {
"entityTypes": {},
"actions": {}
},
}))
.unwrap();
let schema_fragment: ValidatorSchemaFragment = fragment.try_into().unwrap();
assert_eq!(
schema_fragment
.0
.iter()
.map(|f| f.namespace())
.collect::<HashSet<_>>(),
HashSet::from([
&Some("Foo::Bar::Baz".parse().unwrap()),
&Some("Foo".parse().unwrap()),
&Some("Bar".parse().unwrap())
])
);
}
#[test]
fn schema_no_fragments() {
let schema = ValidatorSchema::from_schema_fragments([]).unwrap();
assert!(schema.entity_types.is_empty());
assert!(schema.action_ids.is_empty());
}
#[test]
fn same_action_different_namespace() {
let fragment: SchemaFragment = serde_json::from_value(json!({
"Foo::Bar": {
"entityTypes": {},
"actions": {
"Baz": {}
}
},
"Bar::Foo": {
"entityTypes": {},
"actions": {
"Baz": { }
}
},
"Biz": {
"entityTypes": {},
"actions": {
"Baz": { }
}
}
}))
.unwrap();
let schema: ValidatorSchema = fragment.try_into().unwrap();
assert!(schema
.get_action_id(&"Foo::Bar::Action::\"Baz\"".parse().unwrap())
.is_some());
assert!(schema
.get_action_id(&"Bar::Foo::Action::\"Baz\"".parse().unwrap())
.is_some());
assert!(schema
.get_action_id(&"Biz::Action::\"Baz\"".parse().unwrap())
.is_some());
}
#[test]
fn same_type_different_namespace() {
let fragment: SchemaFragment = serde_json::from_value(json!({
"Foo::Bar": {
"entityTypes": {"Baz" : {}},
"actions": { }
},
"Bar::Foo": {
"entityTypes": {"Baz" : {}},
"actions": { }
},
"Biz": {
"entityTypes": {"Baz" : {}},
"actions": { }
}
}))
.unwrap();
let schema: ValidatorSchema = fragment.try_into().unwrap();
assert!(schema
.get_entity_type(&"Foo::Bar::Baz".parse().unwrap())
.is_some());
assert!(schema
.get_entity_type(&"Bar::Foo::Baz".parse().unwrap())
.is_some());
assert!(schema
.get_entity_type(&"Biz::Baz".parse().unwrap())
.is_some());
}
#[test]
fn member_of_different_namespace() {
let fragment: SchemaFragment = serde_json::from_value(json!({
"Bar": {
"entityTypes": {
"Baz": {
"memberOfTypes": ["Foo::Buz"]
}
},
"actions": {}
},
"Foo": {
"entityTypes": { "Buz": {} },
"actions": { }
}
}))
.unwrap();
let schema: ValidatorSchema = fragment.try_into().unwrap();
let buz = schema
.get_entity_type(&"Foo::Buz".parse().unwrap())
.unwrap();
assert_eq!(
buz.descendants,
HashSet::from(["Bar::Baz".parse().unwrap()])
);
}
#[test]
fn attribute_different_namespace() {
let fragment: SchemaFragment = serde_json::from_value(json!({
"Bar": {
"entityTypes": {
"Baz": {
"shape": {
"type": "Record",
"attributes": {
"fiz": {
"type": "Entity",
"name": "Foo::Buz"
}
}
}
}
},
"actions": {}
},
"Foo": {
"entityTypes": { "Buz": {} },
"actions": { }
}
}))
.unwrap();
let schema: ValidatorSchema = fragment.try_into().unwrap();
let baz = schema
.get_entity_type(&"Bar::Baz".parse().unwrap())
.unwrap();
assert_eq!(
baz.attr("fiz").unwrap().attr_type,
Type::named_entity_reference_from_str("Foo::Buz"),
);
}
#[test]
fn applies_to_different_namespace() {
let fragment: SchemaFragment = serde_json::from_value(json!({
"Foo::Bar": {
"entityTypes": { },
"actions": {
"Baz": {
"appliesTo": {
"principalTypes": [ "Fiz::Buz" ],
"resourceTypes": [ "Fiz::Baz" ],
}
}
}
},
"Fiz": {
"entityTypes": {
"Buz": {},
"Baz": {}
},
"actions": { }
}
}))
.unwrap();
let schema: ValidatorSchema = fragment.try_into().unwrap();
let baz = schema
.get_action_id(&"Foo::Bar::Action::\"Baz\"".parse().unwrap())
.unwrap();
assert_eq!(
baz.applies_to
.applicable_principal_types()
.collect::<HashSet<_>>(),
HashSet::from([&EntityType::Specified("Fiz::Buz".parse().unwrap())])
);
assert_eq!(
baz.applies_to
.applicable_resource_types()
.collect::<HashSet<_>>(),
HashSet::from([&EntityType::Specified("Fiz::Baz".parse().unwrap())])
);
}
#[test]
fn simple_defined_type() {
let fragment: SchemaFragment = serde_json::from_value(json!({
"": {
"commonTypes": {
"MyLong": {"type": "Long"}
},
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"a": {"type": "MyLong"}
}
}
}
},
"actions": {}
}
}))
.unwrap();
let schema: ValidatorSchema = fragment.try_into().unwrap();
assert_eq!(
schema.entity_types.iter().next().unwrap().1.attributes,
Attributes::with_required_attributes([("a".into(), Type::primitive_long())])
);
}
#[test]
fn defined_record_as_attrs() {
let fragment: SchemaFragment = serde_json::from_value(json!({
"": {
"commonTypes": {
"MyRecord": {
"type": "Record",
"attributes": {
"a": {"type": "Long"}
}
}
},
"entityTypes": {
"User": { "shape": { "type": "MyRecord", } }
},
"actions": {}
}
}))
.unwrap();
let schema: ValidatorSchema = fragment.try_into().unwrap();
assert_eq!(
schema.entity_types.iter().next().unwrap().1.attributes,
Attributes::with_required_attributes([("a".into(), Type::primitive_long())])
);
}
#[test]
fn cross_namespace_type() {
let fragment: SchemaFragment = serde_json::from_value(json!({
"A": {
"commonTypes": {
"MyLong": {"type": "Long"}
},
"entityTypes": { },
"actions": {}
},
"B": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"a": {"type": "A::MyLong"}
}
}
}
},
"actions": {}
}
}))
.unwrap();
let schema: ValidatorSchema = fragment.try_into().unwrap();
assert_eq!(
schema.entity_types.iter().next().unwrap().1.attributes,
Attributes::with_required_attributes([("a".into(), Type::primitive_long())])
);
}
#[test]
fn cross_fragment_type() {
let fragment1: ValidatorSchemaFragment = serde_json::from_value::<SchemaFragment>(json!({
"A": {
"commonTypes": {
"MyLong": {"type": "Long"}
},
"entityTypes": { },
"actions": {}
}
}))
.unwrap()
.try_into()
.unwrap();
let fragment2: ValidatorSchemaFragment = serde_json::from_value::<SchemaFragment>(json!({
"A": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"a": {"type": "MyLong"}
}
}
}
},
"actions": {}
}
}))
.unwrap()
.try_into()
.unwrap();
let schema = ValidatorSchema::from_schema_fragments([fragment1, fragment2]).unwrap();
assert_eq!(
schema.entity_types.iter().next().unwrap().1.attributes,
Attributes::with_required_attributes([("a".into(), Type::primitive_long())])
);
}
#[test]
fn cross_fragment_duplicate_type() {
let fragment1: ValidatorSchemaFragment = serde_json::from_value::<SchemaFragment>(json!({
"A": {
"commonTypes": {
"MyLong": {"type": "Long"}
},
"entityTypes": {},
"actions": {}
}
}))
.unwrap()
.try_into()
.unwrap();
let fragment2: ValidatorSchemaFragment = serde_json::from_value::<SchemaFragment>(json!({
"A": {
"commonTypes": {
"MyLong": {"type": "Long"}
},
"entityTypes": {},
"actions": {}
}
}))
.unwrap()
.try_into()
.unwrap();
let schema = ValidatorSchema::from_schema_fragments([fragment1, fragment2]);
match schema {
Err(SchemaError::DuplicateCommonType(s)) if s.contains("A::MyLong") => (),
_ => panic!("should have errored because schema fragments have duplicate types"),
};
}
#[test]
fn undeclared_type_in_attr() {
let fragment: SchemaFragment = serde_json::from_value(json!({
"": {
"commonTypes": { },
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"a": {"type": "MyLong"}
}
}
}
},
"actions": {}
}
}))
.unwrap();
match TryInto::<ValidatorSchema>::try_into(fragment) {
Err(SchemaError::UndeclaredCommonTypes(_)) => (),
s => panic!(
"Expected Err(SchemaError::UndeclaredCommonType), got {:?}",
s
),
}
}
#[test]
fn undeclared_type_in_type_def() {
let fragment: SchemaFragment = serde_json::from_value(json!({
"": {
"commonTypes": {
"a": { "type": "b" }
},
"entityTypes": { },
"actions": {}
}
}))
.unwrap();
match TryInto::<ValidatorSchema>::try_into(fragment) {
Err(SchemaError::UndeclaredCommonTypes(_)) => (),
s => panic!(
"Expected Err(SchemaError::UndeclaredCommonType), got {:?}",
s
),
}
}
#[test]
fn shape_not_record() {
let fragment: SchemaFragment = serde_json::from_value(json!({
"": {
"commonTypes": {
"MyLong": { "type": "Long" }
},
"entityTypes": {
"User": {
"shape": { "type": "MyLong" }
}
},
"actions": {}
}
}))
.unwrap();
match TryInto::<ValidatorSchema>::try_into(fragment) {
Err(SchemaError::ContextOrShapeNotRecord(_)) => (),
s => panic!(
"Expected Err(SchemaError::ContextOrShapeNotRecord), got {:?}",
s
),
}
}
#[test]
fn counterexamples_from_cedar_134() {
let bad1 = json!({
"": {
"entityTypes": {
"User // comment": {
"memberOfTypes": [
"UserGroup"
]
},
"User": {
"memberOfTypes": [
"UserGroup"
]
},
"UserGroup": {}
},
"actions": {}
}
});
let fragment = serde_json::from_value::<SchemaFragment>(bad1); assert!(fragment.is_err());
let bad2 = json!({
"ABC :: //comment \n XYZ ": {
"entityTypes": {
"User": {
"memberOfTypes": []
}
},
"actions": {}
}
});
let fragment = serde_json::from_value::<SchemaFragment>(bad2); assert!(fragment.is_err());
}
#[test]
fn simple_action_entity() {
let src = json!(
{
"entityTypes": { },
"actions": {
"view_photo": { },
}
});
let schema_file: NamespaceDefinition = serde_json::from_value(src).expect("Parse Error");
let schema: ValidatorSchema = schema_file.try_into().expect("Schema Error");
let actions = schema.action_entities().expect("Entity Construct Error");
let action_uid = EntityUID::from_str("Action::\"view_photo\"").unwrap();
let view_photo = actions.entity(&action_uid);
assert_eq!(
view_photo.unwrap(),
&Entity::new_with_attr_partial_value(action_uid, HashMap::new(), HashSet::new())
);
}
#[test]
fn action_entity_hierarchy() {
let src = json!(
{
"entityTypes": { },
"actions": {
"read": {},
"view": {
"memberOf": [{"id": "read"}]
},
"view_photo": {
"memberOf": [{"id": "view"}]
},
}
});
let schema_file: NamespaceDefinition = serde_json::from_value(src).expect("Parse Error");
let schema: ValidatorSchema = schema_file.try_into().expect("Schema Error");
let actions = schema.action_entities().expect("Entity Construct Error");
let view_photo_uid = EntityUID::from_str("Action::\"view_photo\"").unwrap();
let view_uid = EntityUID::from_str("Action::\"view\"").unwrap();
let read_uid = EntityUID::from_str("Action::\"read\"").unwrap();
let view_photo_entity = actions.entity(&view_photo_uid);
assert_eq!(
view_photo_entity.unwrap(),
&Entity::new_with_attr_partial_value(
view_photo_uid,
HashMap::new(),
HashSet::from([view_uid.clone(), read_uid.clone()])
)
);
let view_entity = actions.entity(&view_uid);
assert_eq!(
view_entity.unwrap(),
&Entity::new_with_attr_partial_value(
view_uid,
HashMap::new(),
HashSet::from([read_uid.clone()])
)
);
let read_entity = actions.entity(&read_uid);
assert_eq!(
read_entity.unwrap(),
&Entity::new_with_attr_partial_value(read_uid, HashMap::new(), HashSet::new())
);
}
#[test]
fn action_entity_attribute() {
let src = json!(
{
"entityTypes": { },
"actions": {
"view_photo": {
"attributes": { "attr": "foo" }
},
}
});
let schema_file: NamespaceDefinitionWithActionAttributes =
serde_json::from_value(src).expect("Parse Error");
let schema: ValidatorSchema = schema_file.try_into().expect("Schema Error");
let actions = schema.action_entities().expect("Entity Construct Error");
let action_uid = EntityUID::from_str("Action::\"view_photo\"").unwrap();
let view_photo = actions.entity(&action_uid);
assert_eq!(
view_photo.unwrap(),
&Entity::new(
action_uid,
HashMap::from([("attr".into(), RestrictedExpr::val("foo"))]),
HashSet::new(),
&Extensions::none(),
)
.unwrap(),
);
}
#[test]
fn test_action_namespace_inference_multi_success() {
let src = json!({
"Foo" : {
"entityTypes" : {},
"actions" : {
"read" : {}
}
},
"ExampleCo::Personnel" : {
"entityTypes" : {},
"actions" : {
"viewPhoto" : {
"memberOf" : [
{
"id" : "read",
"type" : "Foo::Action"
}
]
}
}
},
});
let schema_fragment =
serde_json::from_value::<SchemaFragment>(src).expect("Failed to parse schema");
let schema: ValidatorSchema = schema_fragment.try_into().expect("Schema should construct");
let view_photo = schema
.action_entities_iter()
.find(|e| e.uid() == &r#"ExampleCo::Personnel::Action::"viewPhoto""#.parse().unwrap())
.unwrap();
let ancestors = view_photo.ancestors().collect::<Vec<_>>();
let read = ancestors[0];
assert_eq!(read.eid().to_string(), "read");
assert_eq!(read.entity_type().to_string(), "Foo::Action");
}
#[test]
fn test_action_namespace_inference_multi() {
let src = json!({
"ExampleCo::Personnel::Foo" : {
"entityTypes" : {},
"actions" : {
"read" : {}
}
},
"ExampleCo::Personnel" : {
"entityTypes" : {},
"actions" : {
"viewPhoto" : {
"memberOf" : [
{
"id" : "read",
"type" : "Foo::Action"
}
]
}
}
},
});
let schema_fragment =
serde_json::from_value::<SchemaFragment>(src).expect("Failed to parse schema");
let schema: std::result::Result<ValidatorSchema, _> = schema_fragment.try_into();
schema.expect_err("Schema should fail to construct as the normalization rules treat any qualification as starting from the root");
}
#[test]
fn test_action_namespace_inference() {
let src = json!({
"ExampleCo::Personnel" : {
"entityTypes" : { },
"actions" : {
"read" : {},
"viewPhoto" : {
"memberOf" : [
{
"id" : "read",
"type" : "Action"
}
]
}
}
}
});
let schema_fragment =
serde_json::from_value::<SchemaFragment>(src).expect("Failed to parse schema");
let schema: ValidatorSchema = schema_fragment.try_into().unwrap();
let view_photo = schema
.action_entities_iter()
.find(|e| e.uid() == &r#"ExampleCo::Personnel::Action::"viewPhoto""#.parse().unwrap())
.unwrap();
let ancestors = view_photo.ancestors().collect::<Vec<_>>();
let read = ancestors[0];
assert_eq!(read.eid().to_string(), "read");
assert_eq!(
read.entity_type().to_string(),
"ExampleCo::Personnel::Action"
);
}
#[test]
fn qualified_undeclared_common_types() {
let src = json!(
{
"Demo": {
"entityTypes": {
"User": {
"memberOfTypes": [],
"shape": {
"type": "Record",
"attributes": {
"id": { "type": "id" },
}
}
}
},
"actions": {}
},
"": {
"commonTypes": {
"id": {
"type": "String"
},
},
"entityTypes": {},
"actions": {}
}
}
);
let schema = ValidatorSchema::from_json_value(src, Extensions::all_available());
assert_matches!(schema, Err(SchemaError::UndeclaredCommonTypes(types)) =>
assert_eq!(types, HashSet::from(["Demo::id".to_string()])));
}
#[test]
fn qualified_undeclared_common_types2() {
let src = json!(
{
"Demo": {
"entityTypes": {
"User": {
"memberOfTypes": [],
"shape": {
"type": "Record",
"attributes": {
"id": { "type": "Demo::id" },
}
}
}
},
"actions": {}
},
"": {
"commonTypes": {
"id": {
"type": "String"
},
},
"entityTypes": {},
"actions": {}
}
}
);
let schema = ValidatorSchema::from_json_value(src, Extensions::all_available());
assert_matches!(schema, Err(SchemaError::UndeclaredCommonTypes(types)) =>
assert_eq!(types, HashSet::from(["Demo::id".to_string()])));
}
}
#[cfg(test)]
mod test_resolver {
use std::collections::HashMap;
use cedar_policy_core::ast::Name;
use cool_asserts::assert_matches;
use super::CommonTypeResolver;
use crate::{types::Type, SchemaError, SchemaFragment, ValidatorSchemaFragment};
fn resolve(schema: SchemaFragment) -> Result<HashMap<Name, Type>, SchemaError> {
let schema: ValidatorSchemaFragment = schema.try_into().unwrap();
let mut type_defs = HashMap::new();
for def in schema.0 {
type_defs.extend(def.type_defs.type_defs.into_iter());
}
let resolver = CommonTypeResolver::new(&type_defs);
resolver.resolve()
}
#[test]
fn test_simple() {
let schema = serde_json::from_value::<SchemaFragment>(serde_json::json!(
{
"": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "b"
},
"b": {
"type": "Boolean"
}
}
}
}
))
.unwrap();
let res = resolve(schema).unwrap();
assert_eq!(
res,
HashMap::from_iter([
("a".parse().unwrap(), Type::primitive_boolean()),
("b".parse().unwrap(), Type::primitive_boolean())
])
);
let schema = serde_json::from_value::<SchemaFragment>(serde_json::json!(
{
"": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "b"
},
"b": {
"type": "c"
},
"c": {
"type": "Boolean"
}
}
}
}
))
.unwrap();
let res = resolve(schema).unwrap();
assert_eq!(
res,
HashMap::from_iter([
("a".parse().unwrap(), Type::primitive_boolean()),
("b".parse().unwrap(), Type::primitive_boolean()),
("c".parse().unwrap(), Type::primitive_boolean())
])
);
}
#[test]
fn test_set() {
let schema = serde_json::from_value::<SchemaFragment>(serde_json::json!(
{
"": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "Set",
"element": {
"type": "b"
}
},
"b": {
"type": "Boolean"
}
}
}
}
))
.unwrap();
let res = resolve(schema).unwrap();
assert_eq!(
res,
HashMap::from_iter([
("a".parse().unwrap(), Type::set(Type::primitive_boolean())),
("b".parse().unwrap(), Type::primitive_boolean())
])
);
}
#[test]
fn test_record() {
let schema = serde_json::from_value::<SchemaFragment>(serde_json::json!(
{
"": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "Record",
"attributes": {
"foo": {
"type": "b"
}
}
},
"b": {
"type": "Boolean"
}
}
}
}
))
.unwrap();
let res = resolve(schema).unwrap();
assert_eq!(
res,
HashMap::from_iter([
(
"a".parse().unwrap(),
Type::record_with_required_attributes(
std::iter::once(("foo".into(), Type::primitive_boolean())),
crate::types::OpenTag::ClosedAttributes
)
),
("b".parse().unwrap(), Type::primitive_boolean())
])
);
}
#[test]
fn test_names() {
let schema = serde_json::from_value::<SchemaFragment>(serde_json::json!(
{
"A": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "B::a"
}
}
},
"B": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "Boolean"
}
}
}
}
))
.unwrap();
let res = resolve(schema).unwrap();
assert_eq!(
res,
HashMap::from_iter([
("A::a".parse().unwrap(), Type::primitive_boolean()),
("B::a".parse().unwrap(), Type::primitive_boolean())
])
);
}
#[test]
fn test_cycles() {
let schema = serde_json::from_value::<SchemaFragment>(serde_json::json!(
{
"": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "a"
}
}
}
}
))
.unwrap();
let res = resolve(schema);
assert_matches!(res, Err(SchemaError::CycleInCommonTypeReferences(_)));
let schema = serde_json::from_value::<SchemaFragment>(serde_json::json!(
{
"": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "b"
},
"b" : {
"type": "a"
}
}
}
}
))
.unwrap();
let res = resolve(schema);
assert_matches!(res, Err(SchemaError::CycleInCommonTypeReferences(_)));
let schema = serde_json::from_value::<SchemaFragment>(serde_json::json!(
{
"": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "b"
},
"b" : {
"type": "c"
},
"c" : {
"type": "a"
}
}
}
}
))
.unwrap();
let res = resolve(schema);
assert_matches!(res, Err(SchemaError::CycleInCommonTypeReferences(_)));
let schema = serde_json::from_value::<SchemaFragment>(serde_json::json!(
{
"A": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "B::a"
}
}
},
"B": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "A::a"
}
}
}
}
))
.unwrap();
let res = resolve(schema);
assert_matches!(res, Err(SchemaError::CycleInCommonTypeReferences(_)));
let schema = serde_json::from_value::<SchemaFragment>(serde_json::json!(
{
"A": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "B::a"
}
}
},
"B": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "C::a"
}
}
},
"C": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "A::a"
}
}
}
}
))
.unwrap();
let res = resolve(schema);
assert_matches!(res, Err(SchemaError::CycleInCommonTypeReferences(_)));
let schema = serde_json::from_value::<SchemaFragment>(serde_json::json!(
{
"A": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "B::a"
}
}
},
"B": {
"entityTypes": {},
"actions": {},
"commonTypes": {
"a" : {
"type": "c"
},
"c": {
"type": "A::a"
}
}
}
}
))
.unwrap();
let res = resolve(schema);
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<Task>;
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::<HashSet<_>>();
assert_eq!(principals.len(), 1);
let user: EntityType = EntityType::Specified("User".parse().unwrap());
assert!(principals.contains(&user));
let principals = schema.principals().collect::<Vec<_>>();
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::<Vec<_>>().is_empty());
assert!(empty.resources().collect::<Vec<_>>().is_empty());
}
#[test]
fn resources() {
let schema = schema();
let resources = schema.resources().cloned().collect::<HashSet<_>>();
let expected: HashSet<EntityType> = 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::<Vec<_>>();
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::<Vec<_>>();
assert_eq!(got, vec![EntityType::Specified("List".parse().unwrap())]);
let got = schema
.resources_for_action(&create_list)
.unwrap()
.cloned()
.collect::<Vec<_>>();
assert_eq!(
got,
vec![EntityType::Specified("Application".parse().unwrap())]
);
let got = schema
.resources_for_action(&get_list)
.unwrap()
.cloned()
.collect::<HashSet<_>>();
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::<HashSet<_>>();
let expected = HashSet::from(["Team".parse().unwrap(), "Application".parse().unwrap()]);
assert_eq!(parents, expected);
let parents = schema
.ancestors(&"List".parse().unwrap())
.unwrap()
.cloned()
.collect::<HashSet<_>>();
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::<HashSet<_>>();
let expected = HashSet::from([]);
assert_eq!(parents, expected);
}
#[test]
fn action_groups() {
let schema = schema();
let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
let expected = ["Read", "Write", "Create"]
.into_iter()
.map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
.collect::<HashSet<EntityUID>>();
assert_eq!(groups, expected);
}
#[test]
fn actions() {
let schema = schema();
let actions = schema.actions().cloned().collect::<HashSet<_>>();
let expected = [
"Read",
"Write",
"Create",
"DeleteList",
"EditShare",
"UpdateList",
"CreateTask",
"UpdateTask",
"DeleteTask",
"GetList",
"GetLists",
"CreateList",
]
.into_iter()
.map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
.collect::<HashSet<EntityUID>>();
assert_eq!(actions, expected);
}
#[test]
fn entities() {
let schema = schema();
let entities = schema
.entity_types()
.map(|(ty, _)| ty)
.cloned()
.collect::<HashSet<_>>();
let expected = ["List", "Application", "User", "CoolList", "Team"]
.into_iter()
.map(|ty| ty.parse().unwrap())
.collect::<HashSet<Name>>();
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<Task>;
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::<HashSet<_>>();
assert_eq!(principals.len(), 1);
let user: EntityType = EntityType::Specified("Foo::User".parse().unwrap());
assert!(principals.contains(&user));
let principals = schema.principals().collect::<Vec<_>>();
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::<Vec<_>>().is_empty());
assert!(empty.resources().collect::<Vec<_>>().is_empty());
}
#[test]
fn resources() {
let schema = schema();
let resources = schema.resources().cloned().collect::<HashSet<_>>();
let expected: HashSet<EntityType> = 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::<Vec<_>>();
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::<Vec<_>>();
assert_eq!(
got,
vec![EntityType::Specified("Foo::List".parse().unwrap())]
);
let got = schema
.resources_for_action(&create_list)
.unwrap()
.cloned()
.collect::<Vec<_>>();
assert_eq!(
got,
vec![EntityType::Specified("Foo::Application".parse().unwrap())]
);
let got = schema
.resources_for_action(&get_list)
.unwrap()
.cloned()
.collect::<HashSet<_>>();
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::<HashSet<_>>();
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::<HashSet<_>>();
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::<HashSet<_>>();
let expected = HashSet::from([]);
assert_eq!(parents, expected);
}
#[test]
fn action_groups() {
let schema = schema();
let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
let expected = ["Read", "Write", "Create"]
.into_iter()
.map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
.collect::<HashSet<EntityUID>>();
assert_eq!(groups, expected);
}
#[test]
fn actions() {
let schema = schema();
let actions = schema.actions().cloned().collect::<HashSet<_>>();
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::<HashSet<EntityUID>>();
assert_eq!(actions, expected);
}
#[test]
fn entities() {
let schema = schema();
let entities = schema
.entity_types()
.map(|(ty, _)| ty)
.cloned()
.collect::<HashSet<_>>();
let expected = [
"Foo::List",
"Foo::Application",
"Foo::User",
"Foo::CoolList",
"Foo::Team",
]
.into_iter()
.map(|ty| ty.parse().unwrap())
.collect::<HashSet<Name>>();
assert_eq!(entities, expected);
}
}