use std::collections::{hash_map::Entry, HashMap, HashSet};
use std::sync::Arc;
use cedar_policy_core::{
ast::{Eid, Entity, EntityType, EntityUID, Id, Name, RestrictedExpr},
entities::{Entities, JSONValue, TCComputation},
parser::err::ParseErrors,
transitive_closure::{compute_tc, TCNode},
FromNormalizedStr,
};
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use smol_str::SmolStr;
use crate::types::OpenTag;
use crate::{
schema_file_format,
types::{AttributeType, Attributes, EntityRecordKind, Type},
ActionEntityUID, ActionType, SchemaFragment, SchemaType, SchemaTypeVariant, TypeOfAttribute,
SCHEMA_TYPE_VARIANT_TAGS,
};
use super::err::*;
use super::NamespaceDefinition;
pub(crate) static ACTION_ENTITY_TYPE: &str = "Action";
#[test]
fn action_entity_type_parses() {
Id::from_normalized_str(ACTION_ENTITY_TYPE).unwrap();
}
pub(crate) fn is_action_entity_type(ty: &Name) -> bool {
ty.basename().as_ref() == ACTION_ENTITY_TYPE
}
#[derive(Eq, PartialEq, Copy, Clone, Default)]
pub enum ActionBehavior {
#[default]
ProhibitAttributes,
PermitAttributes,
}
#[derive(Debug)]
pub struct ValidatorNamespaceDef {
namespace: Option<Name>,
type_defs: TypeDefs,
entity_types: EntityTypesDef,
actions: ActionsDef,
}
#[derive(Debug)]
pub struct TypeDefs {
type_defs: HashMap<Name, Type>,
}
#[derive(Debug)]
pub struct EntityTypesDef {
entity_types: HashMap<Name, EntityTypeFragment>,
}
#[derive(Debug)]
pub struct EntityTypeFragment {
attributes: WithUnresolvedTypeDefs<Type>,
parents: HashSet<Name>,
}
#[derive(Debug)]
pub struct ActionsDef {
actions: HashMap<EntityUID, ActionFragment>,
}
#[derive(Debug)]
pub struct ActionFragment {
context: WithUnresolvedTypeDefs<Type>,
applies_to: ValidatorApplySpec,
parents: HashSet<EntityUID>,
attribute_types: Attributes,
attributes: HashMap<SmolStr, RestrictedExpr>,
}
type ResolveFunc<T> = dyn FnOnce(&HashMap<Name, Type>) -> Result<T>;
pub enum WithUnresolvedTypeDefs<T> {
WithUnresolved(Box<ResolveFunc<T>>),
WithoutUnresolved(T),
}
impl<T: 'static> WithUnresolvedTypeDefs<T> {
pub fn new(f: impl FnOnce(&HashMap<Name, Type>) -> Result<T> + 'static) -> Self {
Self::WithUnresolved(Box::new(f))
}
pub fn map<U: 'static>(self, f: impl FnOnce(T) -> U + 'static) -> WithUnresolvedTypeDefs<U> {
match self {
Self::WithUnresolved(_) => {
WithUnresolvedTypeDefs::new(|type_defs| self.resolve_type_defs(type_defs).map(f))
}
Self::WithoutUnresolved(v) => WithUnresolvedTypeDefs::WithoutUnresolved(f(v)),
}
}
pub fn resolve_type_defs(self, type_defs: &HashMap<Name, Type>) -> Result<T> {
match self {
WithUnresolvedTypeDefs::WithUnresolved(f) => f(type_defs),
WithUnresolvedTypeDefs::WithoutUnresolved(v) => Ok(v),
}
}
}
impl<T: 'static> From<T> for WithUnresolvedTypeDefs<T> {
fn from(value: T) -> Self {
Self::WithoutUnresolved(value)
}
}
impl<T: std::fmt::Debug> std::fmt::Debug for WithUnresolvedTypeDefs<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WithUnresolvedTypeDefs::WithUnresolved(_) => f.debug_tuple("WithUnresolved").finish(),
WithUnresolvedTypeDefs::WithoutUnresolved(v) => {
f.debug_tuple("WithoutUnresolved").field(v).finish()
}
}
}
}
impl TryInto<ValidatorNamespaceDef> for NamespaceDefinition {
type Error = SchemaError;
fn try_into(self) -> Result<ValidatorNamespaceDef> {
ValidatorNamespaceDef::from_namespace_definition(None, self, ActionBehavior::default())
}
}
impl ValidatorNamespaceDef {
pub fn from_namespace_definition(
namespace: Option<SmolStr>,
namespace_def: NamespaceDefinition,
action_behavior: ActionBehavior,
) -> Result<ValidatorNamespaceDef> {
let mut e_types_ids: HashSet<SmolStr> = HashSet::new();
for name in namespace_def.entity_types.keys() {
if !e_types_ids.insert(name.clone()) {
return Err(SchemaError::DuplicateEntityType(name.to_string()));
}
}
let mut a_name_eids: HashSet<SmolStr> = HashSet::new();
for name in namespace_def.actions.keys() {
if !a_name_eids.insert(name.clone()) {
return Err(SchemaError::DuplicateAction(name.to_string()));
}
}
let schema_namespace = match namespace.as_deref() {
None => None,
Some("") => None, Some(ns) => {
Some(Name::from_normalized_str(ns).map_err(SchemaError::NamespaceParseError)?)
}
};
Self::check_action_behavior(&namespace_def, action_behavior)?;
let type_defs =
Self::build_type_defs(namespace_def.common_types, schema_namespace.as_ref())?;
let actions = Self::build_action_ids(namespace_def.actions, schema_namespace.as_ref())?;
let entity_types =
Self::build_entity_types(namespace_def.entity_types, schema_namespace.as_ref())?;
Ok(ValidatorNamespaceDef {
namespace: schema_namespace,
type_defs,
entity_types,
actions,
})
}
fn is_builtin_type_name(name: &SmolStr) -> bool {
SCHEMA_TYPE_VARIANT_TAGS
.iter()
.any(|type_name| name == type_name)
}
fn build_type_defs(
schema_file_type_def: HashMap<SmolStr, SchemaType>,
schema_namespace: Option<&Name>,
) -> Result<TypeDefs> {
let type_defs = schema_file_type_def
.into_iter()
.map(|(name_str, schema_ty)| -> Result<_> {
if Self::is_builtin_type_name(&name_str) {
return Err(SchemaError::DuplicateCommonType(name_str.to_string()));
}
let name = Self::parse_unqualified_name_with_namespace(
&name_str,
schema_namespace.cloned(),
)
.map_err(SchemaError::CommonTypeParseError)?;
let ty = Self::try_schema_type_into_validator_type(schema_namespace, schema_ty)?
.resolve_type_defs(&HashMap::new())?;
Ok((name, ty))
})
.collect::<Result<HashMap<_, _>>>()?;
Ok(TypeDefs { type_defs })
}
fn build_entity_types(
schema_files_types: HashMap<SmolStr, schema_file_format::EntityType>,
schema_namespace: Option<&Name>,
) -> Result<EntityTypesDef> {
Ok(EntityTypesDef {
entity_types: schema_files_types
.into_iter()
.map(|(name_str, entity_type)| -> Result<_> {
let name = Self::parse_unqualified_name_with_namespace(
&name_str,
schema_namespace.cloned(),
)
.map_err(SchemaError::EntityTypeParseError)?;
let parents = entity_type
.member_of_types
.iter()
.map(|parent| -> Result<_> {
Self::parse_possibly_qualified_name_with_default_namespace(
parent,
schema_namespace,
)
.map_err(SchemaError::EntityTypeParseError)
})
.collect::<Result<HashSet<_>>>()?;
let attributes = Self::try_schema_type_into_validator_type(
schema_namespace,
entity_type.shape.into_inner(),
)?;
Ok((
name,
EntityTypeFragment {
attributes,
parents,
},
))
})
.collect::<Result<HashMap<_, _>>>()?,
})
}
fn jsonval_to_type_helper(v: &JSONValue, action_id: &EntityUID) -> Result<Type> {
match v {
JSONValue::Bool(_) => Ok(Type::primitive_boolean()),
JSONValue::Long(_) => Ok(Type::primitive_long()),
JSONValue::String(_) => Ok(Type::primitive_string()),
JSONValue::Record(r) => {
let mut required_attrs: HashMap<SmolStr, Type> = HashMap::new();
for (k, v_prime) in r {
let t = Self::jsonval_to_type_helper(v_prime, action_id);
match t {
Ok(ty) => required_attrs.insert(k.clone(), ty),
Err(e) => return Err(e),
};
}
Ok(Type::record_with_required_attributes(
required_attrs,
OpenTag::ClosedAttributes,
))
}
JSONValue::Set(v) => match v.get(0) {
None => Err(SchemaError::ActionAttributesContainEmptySet(
action_id.clone(),
)),
Some(element) => {
let element_type = Self::jsonval_to_type_helper(element, action_id);
match element_type {
Ok(t) => Ok(Type::Set {
element_type: Some(Box::new(t)),
}),
Err(_) => element_type,
}
}
},
_ => Err(SchemaError::UnsupportedActionAttributeType(
action_id.clone(),
)),
}
}
fn convert_attr_jsonval_map_to_attributes(
m: HashMap<SmolStr, JSONValue>,
action_id: &EntityUID,
) -> Result<(Attributes, HashMap<SmolStr, RestrictedExpr>)> {
let mut attr_types: HashMap<SmolStr, Type> = HashMap::new();
let mut attr_values: HashMap<SmolStr, RestrictedExpr> = HashMap::new();
for (k, v) in m {
let t = Self::jsonval_to_type_helper(&v, action_id);
match t {
Ok(ty) => attr_types.insert(k.clone(), ty),
Err(e) => return Err(e),
};
#[allow(clippy::expect_used)]
let e = v.into_expr().expect("`Self::jsonval_to_type_helper` will always return `Err` for a `JSONValue` that might make `into_expr` return `Err`");
attr_values.insert(k.clone(), e);
}
Ok((
Attributes::with_required_attributes(attr_types),
attr_values,
))
}
fn build_action_ids(
schema_file_actions: HashMap<SmolStr, ActionType>,
schema_namespace: Option<&Name>,
) -> Result<ActionsDef> {
Ok(ActionsDef {
actions: schema_file_actions
.into_iter()
.map(|(action_id_str, action_type)| -> Result<_> {
let action_id = Self::parse_action_id_with_namespace(
&ActionEntityUID::default_type(action_id_str),
schema_namespace,
)?;
let (principal_types, resource_types, context) = action_type
.applies_to
.map(|applies_to| {
(
applies_to.principal_types,
applies_to.resource_types,
applies_to.context,
)
})
.unwrap_or_default();
let applies_to = ValidatorApplySpec::new(
Self::parse_apply_spec_type_list(principal_types, schema_namespace)?,
Self::parse_apply_spec_type_list(resource_types, schema_namespace)?,
);
let context = Self::try_schema_type_into_validator_type(
schema_namespace,
context.into_inner(),
)?;
let parents = action_type
.member_of
.unwrap_or_default()
.iter()
.map(|parent| -> Result<_> {
Self::parse_action_id_with_namespace(parent, schema_namespace)
})
.collect::<Result<HashSet<_>>>()?;
let (attribute_types, attributes) =
Self::convert_attr_jsonval_map_to_attributes(
action_type.attributes.unwrap_or_default(),
&action_id,
)?;
Ok((
action_id,
ActionFragment {
context,
applies_to,
parents,
attribute_types,
attributes,
},
))
})
.collect::<Result<HashMap<_, _>>>()?,
})
}
fn check_action_behavior(
schema_file: &NamespaceDefinition,
action_behavior: ActionBehavior,
) -> Result<()> {
if schema_file
.entity_types
.iter()
.any(|(name, _)| name == ACTION_ENTITY_TYPE)
{
return Err(SchemaError::ActionEntityTypeDeclared);
}
if action_behavior == ActionBehavior::ProhibitAttributes {
let mut actions_with_attributes: Vec<String> = Vec::new();
for (name, a) in &schema_file.actions {
if a.attributes.is_some() {
actions_with_attributes.push(name.to_string());
}
}
if !actions_with_attributes.is_empty() {
return Err(SchemaError::ActionHasAttributes(actions_with_attributes));
}
}
Ok(())
}
fn parse_record_attributes(
schema_namespace: Option<&Name>,
attrs: impl IntoIterator<Item = (SmolStr, TypeOfAttribute)>,
) -> Result<WithUnresolvedTypeDefs<Attributes>> {
let attrs_with_type_defs = attrs
.into_iter()
.map(|(attr, ty)| -> Result<_> {
Ok((
attr,
(
Self::try_schema_type_into_validator_type(schema_namespace, ty.ty)?,
ty.required,
),
))
})
.collect::<Result<Vec<_>>>()?;
Ok(WithUnresolvedTypeDefs::new(|typ_defs| {
attrs_with_type_defs
.into_iter()
.map(|(s, (attr_ty, is_req))| {
attr_ty
.resolve_type_defs(typ_defs)
.map(|ty| (s, AttributeType::new(ty, is_req)))
})
.collect::<Result<Vec<_>>>()
.map(Attributes::with_attributes)
}))
}
fn parse_apply_spec_type_list(
types: Option<Vec<SmolStr>>,
namespace: Option<&Name>,
) -> Result<HashSet<EntityType>> {
types
.map(|types| {
types
.iter()
.map(|ty_str| {
Ok(EntityType::Concrete(
Self::parse_possibly_qualified_name_with_default_namespace(
ty_str, namespace,
)
.map_err(SchemaError::EntityTypeParseError)?,
))
})
.collect::<Result<HashSet<_>>>()
})
.unwrap_or_else(|| Ok(HashSet::from([EntityType::Unspecified])))
}
pub(crate) fn parse_possibly_qualified_name_with_default_namespace(
name_str: &SmolStr,
default_namespace: Option<&Name>,
) -> std::result::Result<Name, ParseErrors> {
let name = Name::from_normalized_str(name_str)?;
let qualified_name = if name.namespace_components().next().is_some() {
name
} else {
match default_namespace {
Some(namespace) => {
Name::type_in_namespace(name.basename().clone(), namespace.clone())
}
None => name,
}
};
Ok(qualified_name)
}
fn parse_unqualified_name_with_namespace(
type_name: impl AsRef<str>,
namespace: Option<Name>,
) -> std::result::Result<Name, ParseErrors> {
let type_name = Id::from_normalized_str(type_name.as_ref())?;
match namespace {
Some(namespace) => Ok(Name::type_in_namespace(type_name, namespace)),
None => Ok(Name::unqualified_name(type_name)),
}
}
fn parse_action_id_with_namespace(
action_id: &ActionEntityUID,
namespace: Option<&Name>,
) -> Result<EntityUID> {
let namespaced_action_type = if let Some(action_ty) = &action_id.ty {
Self::parse_possibly_qualified_name_with_default_namespace(action_ty, namespace)
.map_err(SchemaError::EntityTypeParseError)?
} else {
#[allow(clippy::expect_used)]
let id = Id::from_normalized_str(ACTION_ENTITY_TYPE).expect(
"Expected that the constant ACTION_ENTITY_TYPE would be a valid entity type.",
);
match namespace {
Some(namespace) => Name::type_in_namespace(id, namespace.clone()),
None => Name::unqualified_name(id),
}
};
Ok(EntityUID::from_components(
namespaced_action_type,
Eid::new(action_id.id.clone()),
))
}
pub(crate) fn try_schema_type_into_validator_type(
default_namespace: Option<&Name>,
schema_ty: SchemaType,
) -> Result<WithUnresolvedTypeDefs<Type>> {
match schema_ty {
SchemaType::Type(SchemaTypeVariant::String) => Ok(Type::primitive_string().into()),
SchemaType::Type(SchemaTypeVariant::Long) => Ok(Type::primitive_long().into()),
SchemaType::Type(SchemaTypeVariant::Boolean) => Ok(Type::primitive_boolean().into()),
SchemaType::Type(SchemaTypeVariant::Set { element }) => Ok(
Self::try_schema_type_into_validator_type(default_namespace, *element)?
.map(Type::set),
),
SchemaType::Type(SchemaTypeVariant::Record {
attributes,
additional_attributes,
}) => {
if additional_attributes {
Err(SchemaError::UnsupportedFeature(
UnsupportedFeature::OpenRecordsAndEntities,
))
} else {
Ok(
Self::parse_record_attributes(default_namespace, attributes)?.map(
|attrs| Type::record_with_attributes(attrs, OpenTag::ClosedAttributes),
),
)
}
}
SchemaType::Type(SchemaTypeVariant::Entity { name }) => {
let entity_type_name = Self::parse_possibly_qualified_name_with_default_namespace(
&name,
default_namespace,
)
.map_err(SchemaError::EntityTypeParseError)?;
Ok(Type::named_entity_reference(entity_type_name).into())
}
SchemaType::Type(SchemaTypeVariant::Extension { name }) => {
let extension_type_name = Name::from_normalized_str(&name)
.map_err(SchemaError::ExtensionTypeParseError)?;
Ok(Type::extension(extension_type_name).into())
}
SchemaType::TypeDef { type_name } => {
let defined_type_name = Self::parse_possibly_qualified_name_with_default_namespace(
&type_name,
default_namespace,
)
.map_err(SchemaError::CommonTypeParseError)?;
Ok(WithUnresolvedTypeDefs::new(move |typ_defs| {
typ_defs.get(&defined_type_name).cloned().ok_or(
SchemaError::UndeclaredCommonTypes(HashSet::from([type_name.to_string()])),
)
}))
}
}
}
pub fn namespace(&self) -> &Option<Name> {
&self.namespace
}
}
#[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())
}
}
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,
) -> Result<Self> {
Ok(Self(
fragment
.0
.into_iter()
.map(|(fragment_ns, ns_def)| {
ValidatorNamespaceDef::from_namespace_definition(
Some(fragment_ns),
ns_def,
action_behavior,
)
})
.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 empty() -> ValidatorSchema {
Self {
entity_types: HashMap::new(),
action_ids: HashMap::new(),
}
}
pub fn from_json_value(json: serde_json::Value) -> Result<Self> {
Self::from_schema_file(
SchemaFragment::from_json_value(json)?,
ActionBehavior::default(),
)
}
pub fn from_file(file: impl std::io::Read) -> Result<Self> {
Self::from_schema_file(SchemaFragment::from_file(file)?, ActionBehavior::default())
}
pub fn from_schema_file(
schema_file: SchemaFragment,
action_behavior: ActionBehavior,
) -> Result<ValidatorSchema> {
Self::from_schema_fragments([ValidatorSchemaFragment::from_schema_fragment(
schema_file,
action_behavior,
)?])
}
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 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();
Ok((
name.clone(),
ValidatorEntityType {
name: name.clone(),
descendants,
attributes: Self::record_attributes_or_none(
entity_type.attributes.resolve_type_defs(&type_defs)?,
)
.ok_or(SchemaError::ContextOrShapeNotRecord(
ContextOrShape::EntityTypeShape(name),
))?,
},
))
})
.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();
Ok((
name.clone(),
ValidatorActionId {
name: name.clone(),
applies_to: action.applies_to,
descendants,
context: Self::record_attributes_or_none(
action.context.resolve_type_defs(&type_defs)?,
)
.ok_or(SchemaError::ContextOrShapeNotRecord(
ContextOrShape::ActionContext(name),
))?,
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() {
for (_, attr_typ) in action.context.iter() {
Self::check_undeclared_in_type(
&attr_typ.attr_type,
entity_types,
&mut undeclared_e,
);
}
for p_entity in action.applies_to.applicable_principal_types() {
match p_entity {
EntityType::Concrete(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::Concrete(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> {
match ty {
Type::EntityOrRecord(EntityRecordKind::Record { attrs, .. }) => Some(attrs),
_ => 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(&self, entity_type_id: &Name) -> Option<&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 {
self.entity_types.contains_key(entity_type)
}
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_eq<'a, H, K>(&self, var: H, euid: EntityUID) -> Option<K>
where
H: 'a + HeadVar<K>,
K: 'a,
{
var.get_euid_component(euid)
}
pub(crate) fn get_entities_in<'a, H, K>(
&'a self,
var: H,
euid: EntityUID,
) -> impl Iterator<Item = K> + 'a
where
H: 'a + HeadVar<K>,
K: 'a + Clone,
{
var.get_descendants_if_present(self, euid.clone())
.into_iter()
.flatten()
.map(Clone::clone)
.chain(var.get_euid_component_if_present(self, euid).into_iter())
}
pub(crate) fn get_entities_in_set<'a, H, K>(
&'a self,
var: H,
euids: impl IntoIterator<Item = EntityUID> + 'a,
) -> impl Iterator<Item = K> + 'a
where
H: 'a + HeadVar<K>,
K: 'a + Clone,
{
euids
.into_iter()
.flat_map(move |e| self.get_entities_in(var, e))
}
pub fn get_context_schema(
&self,
action: &EntityUID,
) -> Option<impl cedar_policy_core::entities::ContextSchema> {
self.get_action_id(action).map(|action_id| {
ContextSchema(crate::types::Type::record_with_attributes(
action_id
.context
.iter()
.map(|(k, v)| (k.clone(), v.clone())),
OpenTag::ClosedAttributes,
))
})
}
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(
action_id.clone(),
action.attributes.clone(),
action_ancestors.remove(action_id).unwrap_or_default(),
)
})
}
pub fn action_entities(&self) -> cedar_policy_core::entities::Result<Entities> {
Entities::from_entities(
self.action_entities_iter(),
TCComputation::AssumeAlreadyComputed,
)
}
}
pub struct CoreSchema<'a> {
schema: &'a ValidatorSchema,
actions: HashMap<EntityUID, Arc<Entity>>,
}
impl<'a> CoreSchema<'a> {
pub fn new(schema: &'a ValidatorSchema) -> Self {
Self {
actions: schema
.action_entities_iter()
.map(|e| (e.uid(), Arc::new(e)))
.collect(),
schema,
}
}
}
impl<'a> cedar_policy_core::entities::Schema for CoreSchema<'a> {
type EntityTypeDescription = EntityTypeDescription;
fn entity_type(
&self,
entity_type: &cedar_policy_core::ast::EntityType,
) -> Option<EntityTypeDescription> {
match entity_type {
cedar_policy_core::ast::EntityType::Unspecified => None, cedar_policy_core::ast::EntityType::Concrete(name) => {
EntityTypeDescription::new(self.schema, name)
}
}
}
fn action(&self, action: &EntityUID) -> Option<Arc<cedar_policy_core::ast::Entity>> {
self.actions.get(action).map(Arc::clone)
}
fn entity_types_with_basename<'b>(
&'b self,
basename: &'b Id,
) -> Box<dyn Iterator<Item = EntityType> + 'b> {
Box::new(self.schema.entity_types().filter_map(move |(name, _)| {
if name.basename() == basename {
Some(EntityType::Concrete(name.clone()))
} else {
None
}
}))
}
}
pub struct EntityTypeDescription {
core_type: cedar_policy_core::ast::EntityType,
validator_type: ValidatorEntityType,
allowed_parent_types: Arc<HashSet<cedar_policy_core::ast::EntityType>>,
}
impl EntityTypeDescription {
pub fn new(schema: &ValidatorSchema, type_name: &Name) -> Option<Self> {
Some(Self {
core_type: cedar_policy_core::ast::EntityType::Concrete(type_name.clone()),
validator_type: schema.get_entity_type(type_name).cloned()?,
allowed_parent_types: {
let mut set = HashSet::new();
for (possible_parent_typename, possible_parent_et) in &schema.entity_types {
if possible_parent_et.descendants.contains(type_name) {
set.insert(cedar_policy_core::ast::EntityType::Concrete(
possible_parent_typename.clone(),
));
}
}
Arc::new(set)
},
})
}
}
impl cedar_policy_core::entities::EntityTypeDescription for EntityTypeDescription {
fn entity_type(&self) -> cedar_policy_core::ast::EntityType {
self.core_type.clone()
}
fn attr_type(&self, attr: &str) -> Option<cedar_policy_core::entities::SchemaType> {
let attr_type: &crate::types::Type = &self.validator_type.attr(attr)?.attr_type;
#[allow(clippy::expect_used)]
let core_schema_type: cedar_policy_core::entities::SchemaType = attr_type
.clone()
.try_into()
.expect("failed to convert validator type into Core SchemaType");
debug_assert!(attr_type.is_consistent_with(&core_schema_type));
Some(core_schema_type)
}
fn required_attrs<'s>(&'s self) -> Box<dyn Iterator<Item = SmolStr> + 's> {
Box::new(
self.validator_type
.attributes
.iter()
.filter(|(_, ty)| ty.is_required)
.map(|(attr, _)| attr.clone()),
)
}
fn allowed_parent_types(&self) -> Arc<HashSet<cedar_policy_core::ast::EntityType>> {
Arc::clone(&self.allowed_parent_types)
}
}
struct ContextSchema(crate::types::Type);
impl cedar_policy_core::entities::ContextSchema for ContextSchema {
fn context_type(&self) -> cedar_policy_core::entities::SchemaType {
#[allow(clippy::expect_used)]
self.0
.clone()
.try_into()
.expect("failed to convert validator type into Core SchemaType")
}
}
#[derive(Clone, Debug, Serialize)]
pub struct ValidatorEntityType {
pub(crate) name: Name,
pub descendants: HashSet<Name>,
pub(crate) attributes: Attributes,
}
impl ValidatorEntityType {
pub fn attr(&self, attr: &str) -> Option<&AttributeType> {
self.attributes.get_attr(attr)
}
pub fn attributes(&self) -> impl Iterator<Item = (&SmolStr, &AttributeType)> {
self.attributes.iter()
}
}
impl TCNode<Name> for ValidatorEntityType {
fn get_key(&self) -> Name {
self.name.clone()
}
fn add_edge_to(&mut self, k: Name) {
self.descendants.insert(k);
}
fn out_edges(&self) -> Box<dyn Iterator<Item = &Name> + '_> {
Box::new(self.descendants.iter())
}
fn has_edge_to(&self, e: &Name) -> bool {
self.descendants.contains(e)
}
}
#[derive(Clone, Debug, Serialize)]
pub struct ValidatorActionId {
pub(crate) name: EntityUID,
#[serde(rename = "appliesTo")]
pub(crate) applies_to: ValidatorApplySpec,
pub(crate) descendants: HashSet<EntityUID>,
pub(crate) context: Attributes,
pub(crate) attribute_types: Attributes,
pub(crate) attributes: HashMap<SmolStr, RestrictedExpr>,
}
impl ValidatorActionId {
pub fn context(&self) -> impl Iterator<Item = (&SmolStr, &AttributeType)> {
self.context.iter()
}
}
impl TCNode<EntityUID> for ValidatorActionId {
fn get_key(&self) -> EntityUID {
self.name.clone()
}
fn add_edge_to(&mut self, k: EntityUID) {
self.descendants.insert(k);
}
fn out_edges(&self) -> Box<dyn Iterator<Item = &EntityUID> + '_> {
Box::new(self.descendants.iter())
}
fn has_edge_to(&self, e: &EntityUID) -> bool {
self.descendants.contains(e)
}
}
#[derive(Clone, Debug, Serialize)]
pub(crate) struct ValidatorApplySpec {
#[serde(rename = "principalApplySpec")]
principal_apply_spec: HashSet<EntityType>,
#[serde(rename = "resourceApplySpec")]
resource_apply_spec: HashSet<EntityType>,
}
impl ValidatorApplySpec {
pub(crate) fn new(
principal_apply_spec: HashSet<EntityType>,
resource_apply_spec: HashSet<EntityType>,
) -> Self {
Self {
principal_apply_spec,
resource_apply_spec,
}
}
pub(crate) fn applicable_principal_types(&self) -> impl Iterator<Item = &EntityType> {
self.principal_apply_spec.iter()
}
pub(crate) fn applicable_resource_types(&self) -> impl Iterator<Item = &EntityType> {
self.resource_apply_spec.iter()
}
}
pub(crate) trait HeadVar<K>: Copy {
fn get_known_vars<'a>(
&self,
schema: &'a ValidatorSchema,
) -> Box<dyn Iterator<Item = &'a K> + 'a>;
fn get_euid_component(&self, euid: EntityUID) -> Option<K>;
fn get_euid_component_if_present(&self, schema: &ValidatorSchema, euid: EntityUID)
-> Option<K>;
fn get_descendants_if_present<'a>(
&self,
schema: &'a ValidatorSchema,
euid: EntityUID,
) -> Option<Box<dyn Iterator<Item = &'a K> + 'a>>;
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum PrincipalOrResourceHeadVar {
PrincipalOrResource,
}
impl HeadVar<Name> for PrincipalOrResourceHeadVar {
fn get_known_vars<'a>(
&self,
schema: &'a ValidatorSchema,
) -> Box<dyn Iterator<Item = &'a Name> + 'a> {
Box::new(schema.known_entity_types())
}
fn get_euid_component(&self, euid: EntityUID) -> Option<Name> {
let (ty, _) = euid.components();
match ty {
EntityType::Unspecified => None,
EntityType::Concrete(name) => Some(name),
}
}
fn get_euid_component_if_present(
&self,
schema: &ValidatorSchema,
euid: EntityUID,
) -> Option<Name> {
let euid_component = self.get_euid_component(euid)?;
if schema.is_known_entity_type(&euid_component) {
Some(euid_component)
} else {
None
}
}
fn get_descendants_if_present<'a>(
&self,
schema: &'a ValidatorSchema,
euid: EntityUID,
) -> Option<Box<dyn Iterator<Item = &'a Name> + 'a>> {
let euid_component = self.get_euid_component(euid)?;
match schema.get_entity_type(&euid_component) {
Some(entity_type) => Some(Box::new(entity_type.descendants.iter())),
None => None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum ActionHeadVar {
Action,
}
impl HeadVar<EntityUID> for ActionHeadVar {
fn get_known_vars<'a>(
&self,
schema: &'a ValidatorSchema,
) -> Box<dyn Iterator<Item = &'a EntityUID> + 'a> {
Box::new(schema.known_action_ids())
}
fn get_euid_component(&self, euid: EntityUID) -> Option<EntityUID> {
Some(euid)
}
fn get_euid_component_if_present(
&self,
schema: &ValidatorSchema,
euid: EntityUID,
) -> Option<EntityUID> {
let euid_component = self.get_euid_component(euid)?;
if schema.is_known_action_id(&euid_component) {
Some(euid_component)
} else {
None
}
}
fn get_descendants_if_present<'a>(
&self,
schema: &'a ValidatorSchema,
euid: EntityUID,
) -> Option<Box<dyn Iterator<Item = &'a EntityUID> + 'a>> {
let euid_component = self.get_euid_component(euid)?;
match schema.get_action_id(&euid_component) {
Some(action_id) => Some(Box::new(action_id.descendants.iter())),
None => None,
}
}
}
#[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,
)?,
])])
}
}
#[cfg(test)]
mod test {
use std::{collections::BTreeMap, str::FromStr};
use crate::types::Type;
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();
match schema {
Ok(_) => panic!("from_schema_file should have failed"),
Err(SchemaError::CycleInActionHierarchy) => (), e => panic!("Unexpected error from from_schema_file: {:?}", e),
}
}
#[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();
match schema {
Ok(x) => {
println!("{:?}", x);
panic!("from_schema_file should have failed");
}
Err(SchemaError::CycleInActionHierarchy) => (), e => panic!("Unexpected error from from_schema_file: {:?}", e),
}
}
#[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::Concrete(user_entity_type.clone())]
);
assert_eq!(
apply_spec.applicable_resource_types().collect::<Vec<_>>(),
vec![&EntityType::Concrete(photo_entity_type.clone())]
);
}
#[test]
fn cant_use_namespace_in_entity_type() {
let src = r#"
{
"entityTypes": { "NS::User": {} },
"actions": {}
}
"#;
let schema_file: NamespaceDefinition = serde_json::from_str(src).expect("Parse Error");
assert!(
matches!(TryInto::<ValidatorSchema>::try_into(schema_file), Err(SchemaError::EntityTypeParseError(_))),
"Expected that namespace in the entity type NS::User would cause a EntityType parse error.");
}
#[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,
);
match schema {
Err(SchemaError::ActionHasAttributes(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".into() })
);
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".into()
})
);
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: SchemaType = serde_json::from_value(src).expect("Parse Error");
assert_eq!(
schema_ty,
SchemaType::Type(SchemaTypeVariant::Entity {
name: "::Foo".into()
})
);
match ValidatorNamespaceDef::try_schema_type_into_validator_type(
Some(&Name::parse_unqualified_name("NS").expect("Expected namespace.")),
schema_ty,
) {
Err(SchemaError::EntityTypeParseError(_)) => (),
_ => panic!("Did not see expected EntityTypeParseError."),
}
}
#[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::Concrete("Fiz::Buz".parse().unwrap())])
);
assert_eq!(
baz.applies_to
.applicable_resource_types()
.collect::<HashSet<_>>(),
HashSet::from([&EntityType::Concrete("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]
#[should_panic]
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]).unwrap();
assert_eq!(
schema.entity_types.iter().next().unwrap().1.attributes,
Attributes::with_required_attributes([("a".into(), Type::primitive_long())])
);
}
#[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)
.expect("constructing the fragment itself should succeed"); let err = ValidatorSchema::try_from(fragment)
.expect_err("should error due to invalid entity type name");
assert!(
err.to_string()
.contains("needs to be normalized (e.g., whitespace removed): User // comment"),
"actual error message was {err}"
);
let bad2 = json!({
"ABC :: //comment \n XYZ ": {
"entityTypes": {
"User": {
"memberOfTypes": []
}
},
"actions": {}
}
});
let fragment = serde_json::from_value::<SchemaFragment>(bad2)
.expect("constructing the fragment itself should succeed"); let err = ValidatorSchema::try_from(fragment)
.expect_err("should error due to invalid schema namespace");
assert!(
err.to_string().contains(
"needs to be normalized (e.g., whitespace removed): ABC :: //comment "
),
"actual error message was {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(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(
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(view_uid, HashMap::new(), HashSet::from([read_uid.clone()]))
);
let read_entity = actions.entity(&read_uid);
assert_eq!(
read_entity.unwrap(),
&Entity::new(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()
)
);
}
#[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"
);
}
}