use std::collections::{BTreeMap, HashMap, HashSet};
use cedar_policy_core::{
ast::{
Eid, EntityAttrEvaluationError, EntityType, EntityUID, Id, Name,
PartialValueSerializedAsExpr,
},
entities::{CedarValueJson, JsonDeserializationErrorContext},
evaluator::RestrictedEvaluator,
extensions::Extensions,
FromNormalizedStr,
};
use smol_str::{SmolStr, ToSmolStr};
use super::ValidatorApplySpec;
use crate::{
err::*,
is_builtin_type_name, schema_file_format,
types::{AttributeType, Attributes, OpenTag, Type},
ActionBehavior, ActionEntityUID, ActionType, NamespaceDefinition, SchemaType,
SchemaTypeVariant, TypeOfAttribute,
};
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(Debug)]
pub struct ValidatorNamespaceDef {
namespace: Option<Name>,
pub(super) type_defs: TypeDefs,
pub(super) entity_types: EntityTypesDef,
pub(super) actions: ActionsDef,
}
#[derive(Debug)]
pub struct TypeDefs {
pub(super) type_defs: HashMap<Name, SchemaType>,
}
#[derive(Debug)]
pub struct EntityTypesDef {
pub(super) entity_types: HashMap<Name, EntityTypeFragment>,
}
#[derive(Debug)]
pub struct EntityTypeFragment {
pub(super) attributes: WithUnresolvedTypeDefs<Type>,
pub(super) parents: HashSet<Name>,
}
#[derive(Debug)]
pub struct ActionsDef {
pub(super) actions: HashMap<EntityUID, ActionFragment>,
}
#[derive(Debug)]
pub struct ActionFragment {
pub(super) context: WithUnresolvedTypeDefs<Type>,
pub(super) applies_to: ValidatorApplySpec,
pub(super) parents: HashSet<EntityUID>,
pub(super) attribute_types: Attributes,
pub(super) attributes: BTreeMap<SmolStr, PartialValueSerializedAsExpr>,
}
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(),
Extensions::all_available(),
)
}
}
impl ValidatorNamespaceDef {
pub fn from_namespace_definition(
namespace: Option<Name>,
namespace_def: NamespaceDefinition,
action_behavior: ActionBehavior,
extensions: Extensions<'_>,
) -> Result<ValidatorNamespaceDef> {
let mut e_types_ids: HashSet<Id> = 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()));
}
}
Self::check_action_behavior(&namespace_def, action_behavior)?;
let type_defs = Self::build_type_defs(namespace_def.common_types, namespace.as_ref())?;
let actions =
Self::build_action_ids(namespace_def.actions, namespace.as_ref(), extensions)?;
let entity_types =
Self::build_entity_types(namespace_def.entity_types, namespace.as_ref())?;
Ok(ValidatorNamespaceDef {
namespace,
type_defs,
entity_types,
actions,
})
}
fn build_type_defs(
schema_file_type_def: HashMap<Id, SchemaType>,
schema_namespace: Option<&Name>,
) -> Result<TypeDefs> {
let type_defs = schema_file_type_def
.into_iter()
.map(|(name, schema_ty)| -> Result<_> {
let name_str = name.clone().into_smolstr();
if is_builtin_type_name(&name_str) {
return Err(SchemaError::DuplicateCommonType(name_str.to_string()));
}
let name =
Name::from(name).prefix_namespace_if_unqualified(schema_namespace.cloned());
Ok((
name,
schema_ty
.prefix_common_type_references_with_namespace(schema_namespace.cloned()),
))
})
.collect::<Result<HashMap<_, _>>>()?;
Ok(TypeDefs { type_defs })
}
fn build_entity_types(
schema_files_types: HashMap<Id, schema_file_format::EntityType>,
schema_namespace: Option<&Name>,
) -> Result<EntityTypesDef> {
Ok(EntityTypesDef {
entity_types: schema_files_types
.into_iter()
.map(|(id, entity_type)| -> Result<_> {
let name =
Name::from(id).prefix_namespace_if_unqualified(schema_namespace.cloned());
let parents = entity_type
.member_of_types
.into_iter()
.map(|ty| ty.prefix_namespace_if_unqualified(schema_namespace.cloned()))
.collect();
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: &CedarValueJson, action_id: &EntityUID) -> Result<Type> {
match v {
CedarValueJson::Bool(_) => Ok(Type::primitive_boolean()),
CedarValueJson::Long(_) => Ok(Type::primitive_long()),
CedarValueJson::String(_) => Ok(Type::primitive_string()),
CedarValueJson::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,
))
}
CedarValueJson::Set(v) => match v.first() {
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,
}
}
},
CedarValueJson::EntityEscape { __entity: _ } => {
Err(SchemaError::UnsupportedActionAttribute(
action_id.clone(),
"entity escape (`__entity`)".to_owned(),
))
}
CedarValueJson::ExprEscape { __expr: _ } => {
Err(SchemaError::UnsupportedActionAttribute(
action_id.clone(),
"expression escape (`__expr`)".to_owned(),
))
}
CedarValueJson::ExtnEscape { __extn: _ } => {
Err(SchemaError::UnsupportedActionAttribute(
action_id.clone(),
"extension function escape (`__extn`)".to_owned(),
))
}
CedarValueJson::Null => Err(SchemaError::UnsupportedActionAttribute(
action_id.clone(),
"null".to_owned(),
)),
}
}
fn convert_attr_jsonval_map_to_attributes(
m: HashMap<SmolStr, CedarValueJson>,
action_id: &EntityUID,
extensions: Extensions<'_>,
) -> Result<(Attributes, BTreeMap<SmolStr, PartialValueSerializedAsExpr>)> {
let mut attr_types: HashMap<SmolStr, Type> = HashMap::with_capacity(m.len());
let mut attr_values: BTreeMap<SmolStr, PartialValueSerializedAsExpr> = BTreeMap::new();
let evaluator = RestrictedEvaluator::new(&extensions);
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(|| JsonDeserializationErrorContext::EntityAttribute { uid: action_id.clone(), attr: k.clone() }).expect("`Self::jsonval_to_type_helper` will always return `Err` for a `CedarValueJson` that might make `into_expr` return `Err`");
let pv = evaluator
.partial_interpret(e.as_borrowed())
.map_err(|err| {
SchemaError::ActionAttrEval(EntityAttrEvaluationError {
uid: action_id.clone(),
attr: k.clone(),
err,
})
})?;
attr_values.insert(k.clone(), pv.into());
}
Ok((
Attributes::with_required_attributes(attr_types),
attr_values,
))
}
fn build_action_ids(
schema_file_actions: HashMap<SmolStr, ActionType>,
schema_namespace: Option<&Name>,
extensions: Extensions<'_>,
) -> 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| {
Self::parse_action_id_with_namespace(parent, schema_namespace)
})
.collect::<HashSet<_>>();
let (attribute_types, attributes) =
Self::convert_attr_jsonval_map_to_attributes(
action_type.attributes.unwrap_or_default(),
&action_id,
extensions,
)?;
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.to_smolstr() == 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::UnsupportedFeature(
UnsupportedFeature::ActionAttributes(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<Name>>,
namespace: Option<&Name>,
) -> HashSet<EntityType> {
types
.map(|types| {
types
.iter()
.map(|ty| {
EntityType::Specified(
ty.prefix_namespace_if_unqualified(namespace.cloned()),
)
})
.collect::<HashSet<_>>()
})
.unwrap_or_else(|| HashSet::from([EntityType::Unspecified]))
}
fn parse_action_id_with_namespace(
action_id: &ActionEntityUID,
namespace: Option<&Name>,
) -> EntityUID {
let namespaced_action_type = if let Some(action_ty) = &action_id.ty {
action_ty.prefix_namespace_if_unqualified(namespace.cloned())
} 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),
None => Name::unqualified_name(id),
}
};
EntityUID::from_components(namespaced_action_type, Eid::new(action_id.id.clone()), None)
}
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 cfg!(not(feature = "partial-validate")) && additional_attributes {
Err(SchemaError::UnsupportedFeature(
UnsupportedFeature::OpenRecordsAndEntities,
))
} else {
Ok(
Self::parse_record_attributes(default_namespace, attributes)?.map(
move |attrs| {
Type::record_with_attributes(
attrs,
if additional_attributes {
OpenTag::OpenAttributes
} else {
OpenTag::ClosedAttributes
},
)
},
),
)
}
}
SchemaType::Type(SchemaTypeVariant::Entity { name }) => {
Ok(Type::named_entity_reference(
name.prefix_namespace_if_unqualified(default_namespace.cloned()),
)
.into())
}
SchemaType::Type(SchemaTypeVariant::Extension { name }) => {
let extension_type_name = Name::unqualified_name(name);
Ok(Type::extension(extension_type_name).into())
}
SchemaType::TypeDef { type_name } => {
let defined_type_name =
type_name.prefix_namespace_if_unqualified(default_namespace.cloned());
Ok(WithUnresolvedTypeDefs::new(move |typ_defs| {
typ_defs.get(&defined_type_name).cloned().ok_or(
SchemaError::UndeclaredCommonTypes(HashSet::from([
defined_type_name.to_string()
])),
)
}))
}
}
}
pub fn namespace(&self) -> &Option<Name> {
&self.namespace
}
}