use cedar_policy_core::{
ast::{self, EntityType, EntityUID, PartialValueSerializedAsExpr},
transitive_closure::TCNode,
};
use itertools::Itertools;
use nonempty::NonEmpty;
use serde::Serialize;
use smol_str::SmolStr;
use std::collections::{BTreeMap, HashSet};
use super::internal_name_to_entity_type;
use crate::{
schema::{AllDefs, SchemaError},
types::{Attributes, Type},
ConditionalName,
};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidatorActionId {
pub(crate) name: EntityUID,
pub(crate) applies_to: ValidatorApplySpec<ast::EntityType>,
pub(crate) descendants: HashSet<EntityUID>,
pub(crate) context: Type,
pub(crate) attribute_types: Attributes,
pub(crate) attributes: BTreeMap<SmolStr, PartialValueSerializedAsExpr>,
}
impl ValidatorActionId {
pub fn principals(&self) -> impl Iterator<Item = &EntityType> {
self.applies_to.principal_apply_spec.iter()
}
pub fn resources(&self) -> impl Iterator<Item = &EntityType> {
self.applies_to.resource_apply_spec.iter()
}
pub fn context_type(&self) -> &Type {
&self.context
}
pub fn applies_to_principals(&self) -> impl Iterator<Item = &ast::EntityType> {
self.applies_to.applicable_principal_types()
}
pub fn applies_to_resources(&self) -> impl Iterator<Item = &ast::EntityType> {
self.applies_to.applicable_resource_types()
}
pub fn is_applicable_principal_type(&self, ty: &ast::EntityType) -> bool {
self.applies_to.is_applicable_principal_type(ty)
}
pub fn is_applicable_resource_type(&self, ty: &ast::EntityType) -> bool {
self.applies_to.is_applicable_resource_type(ty)
}
}
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)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ValidatorApplySpec<N> {
principal_apply_spec: HashSet<N>,
resource_apply_spec: HashSet<N>,
}
impl<N> ValidatorApplySpec<N> {
pub fn new(principal_apply_spec: HashSet<N>, resource_apply_spec: HashSet<N>) -> Self {
Self {
principal_apply_spec,
resource_apply_spec,
}
}
}
impl ValidatorApplySpec<ast::EntityType> {
pub fn is_applicable_principal_type(&self, ty: &ast::EntityType) -> bool {
self.principal_apply_spec.contains(ty)
}
pub fn applicable_principal_types(&self) -> impl Iterator<Item = &ast::EntityType> {
self.principal_apply_spec.iter()
}
pub fn is_applicable_resource_type(&self, ty: &ast::EntityType) -> bool {
self.resource_apply_spec.contains(ty)
}
pub fn applicable_resource_types(&self) -> impl Iterator<Item = &ast::EntityType> {
self.resource_apply_spec.iter()
}
}
impl ValidatorApplySpec<ConditionalName> {
pub fn fully_qualify_type_references(
self,
all_defs: &AllDefs,
) -> Result<ValidatorApplySpec<ast::EntityType>, crate::schema::SchemaError> {
let (principal_apply_spec, principal_errs) = self
.principal_apply_spec
.into_iter()
.map(|cname| {
let internal_name = cname.resolve(all_defs)?.clone();
internal_name_to_entity_type(internal_name).map_err(Into::into)
})
.partition_result::<_, Vec<SchemaError>, _, _>();
let (resource_apply_spec, resource_errs) = self
.resource_apply_spec
.into_iter()
.map(|cname| {
let internal_name = cname.resolve(all_defs)?.clone();
internal_name_to_entity_type(internal_name).map_err(Into::into)
})
.partition_result::<_, Vec<SchemaError>, _, _>();
match (
NonEmpty::from_vec(principal_errs),
NonEmpty::from_vec(resource_errs),
) {
(None, None) => Ok(ValidatorApplySpec {
principal_apply_spec,
resource_apply_spec,
}),
(Some(principal_errs), None) => Err(SchemaError::join_nonempty(principal_errs)),
(None, Some(resource_errs)) => Err(SchemaError::join_nonempty(resource_errs)),
(Some(principal_errs), Some(resource_errs)) => {
let mut errs = principal_errs;
errs.extend(resource_errs);
Err(SchemaError::join_nonempty(errs))
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
fn make_action() -> ValidatorActionId {
ValidatorActionId {
name: r#"Action::"foo""#.parse().unwrap(),
applies_to: ValidatorApplySpec {
principal_apply_spec: HashSet::from([
"User".parse().unwrap(),
"User".parse().unwrap(),
]),
resource_apply_spec: HashSet::from([
"App".parse().unwrap(),
"File".parse().unwrap(),
]),
},
descendants: HashSet::new(),
context: Type::any_record(),
attribute_types: Attributes::default(),
attributes: BTreeMap::default(),
}
}
#[test]
fn test_resources() {
let a = make_action();
let got = a.resources().cloned().collect::<HashSet<EntityType>>();
let expected = HashSet::from(["App".parse().unwrap(), "File".parse().unwrap()]);
assert_eq!(got, expected);
}
#[test]
fn test_principals() {
let a = make_action();
let got = a.principals().cloned().collect::<Vec<EntityType>>();
let expected: [EntityType; 1] = ["User".parse().unwrap()];
assert_eq!(got, &expected);
}
}