use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use crate::ast::{
BinaryOp, EntityUID, Expr, ExprKind, Literal, PolicySet, RequestType, UnaryOp, Var,
};
use crate::entities::err::EntitiesError;
use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use smol_str::SmolStr;
use thiserror::Error;
mod analysis;
mod loader;
pub mod slicing;
mod type_annotations;
use crate::validator::entity_manifest::analysis::{
EntityManifestAnalysisResult, WrappedAccessPaths,
};
use crate::validator::{
typecheck::{PolicyCheck, Typechecker},
types::Type,
ValidationMode, ValidatorSchema,
};
use crate::validator::{ValidationResult, Validator};
#[doc = include_str!("../../experimental_warning.md")]
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EntityManifest {
#[serde_as(as = "Vec<(_, _)>")]
pub(crate) per_action: HashMap<RequestType, RootAccessTrie>,
}
#[doc = include_str!("../../experimental_warning.md")]
pub type Fields = HashMap<SmolStr, Box<AccessTrie>>;
#[doc = include_str!("../../experimental_warning.md")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[serde(rename_all = "camelCase")]
pub enum EntityRoot {
Literal(EntityUID),
Var(Var),
}
impl Display for EntityRoot {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
EntityRoot::Literal(l) => write!(f, "{l}"),
EntityRoot::Var(v) => write!(f, "{v}"),
}
}
}
#[doc = include_str!("../../experimental_warning.md")]
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RootAccessTrie {
#[serde_as(as = "Vec<(_, _)>")]
pub(crate) trie: HashMap<EntityRoot, AccessTrie>,
}
#[doc = include_str!("../../experimental_warning.md")]
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccessTrie {
#[serde_as(as = "Vec<(_, _)>")]
pub(crate) children: Fields,
pub(crate) ancestors_trie: RootAccessTrie,
pub(crate) is_ancestor: bool,
#[serde(skip_serializing)]
#[serde(skip_deserializing)]
pub(crate) node_type: Option<Type>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct AccessPath {
pub root: EntityRoot,
pub path: Vec<SmolStr>,
}
#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
#[error("entity slicing requires fully concrete policies. Got a policy with an unknown expression")]
pub struct PartialExpressionError {}
impl Diagnostic for PartialExpressionError {}
#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
#[error("entity slicing requires a fully concrete request. Got a partial request")]
pub struct PartialRequestError {}
impl Diagnostic for PartialRequestError {}
#[derive(Debug, Error)]
pub enum EntityManifestError {
#[error("a validation error occurred")]
Validation(ValidationResult),
#[error(transparent)]
Entities(#[from] EntitiesError),
#[error(transparent)]
PartialRequest(#[from] PartialRequestError),
#[error(transparent)]
PartialExpression(#[from] PartialExpressionError),
#[error(transparent)]
UnsupportedCedarFeature(#[from] UnsupportedCedarFeatureError),
}
#[derive(Debug, Clone, Error, Diagnostic)]
#[error("entity manifest analysis currently doesn't support Cedar feature: {feature}")]
pub struct UnsupportedCedarFeatureError {
pub(crate) feature: SmolStr,
}
#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
#[error("entity manifest doesn't match schema. Schema is missing entity {entity}. Either you wrote an entity manifest by hand (not recommended) or you are using an out-of-date entity manifest with respect to the schema")]
pub struct MismatchedMissingEntityError {
pub(crate) entity: EntityUID,
}
#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
#[error("entity manifests are only compatible with schemas that validate in strict mode. Tried to use an invalid schema with an entity manifest")]
pub struct MismatchedNotStrictSchemaError {}
#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
pub enum MismatchedEntityManifestError {
#[error(transparent)]
MismatchedMissingEntity(#[from] MismatchedMissingEntityError),
#[error(transparent)]
MismatchedNotStrictSchema(#[from] MismatchedNotStrictSchemaError),
}
#[derive(Debug, Error)]
pub enum EntityManifestFromJsonError {
#[error(transparent)]
SerdeJsonParseError(#[from] serde_json::Error),
#[error(transparent)]
MismatchedEntityManifest(#[from] MismatchedEntityManifestError),
}
impl EntityManifest {
pub fn per_action(&self) -> &HashMap<RequestType, RootAccessTrie> {
&self.per_action
}
pub fn from_json_str(
json: &str,
schema: &ValidatorSchema,
) -> Result<Self, EntityManifestFromJsonError> {
match serde_json::from_str::<EntityManifest>(json) {
Ok(manifest) => manifest.to_typed(schema).map_err(|e| e.into()),
Err(e) => Err(e.into()),
}
}
pub fn from_json_value(
value: serde_json::Value,
schema: &ValidatorSchema,
) -> Result<Self, EntityManifestFromJsonError> {
match serde_json::from_value::<EntityManifest>(value) {
Ok(manifest) => manifest.to_typed(schema).map_err(|e| e.into()),
Err(e) => Err(e.into()),
}
}
}
fn union_fields_mut(first: &mut Fields, second: Fields) {
for (key, value) in second {
match first.entry(key) {
Entry::Occupied(mut occupied) => {
occupied.get_mut().union_mut(*value);
}
Entry::Vacant(vacant) => {
vacant.insert(value);
}
}
}
}
impl AccessPath {
pub fn to_root_access_trie(&self) -> RootAccessTrie {
self.to_root_access_trie_with_leaf(AccessTrie::default())
}
pub(crate) fn to_root_access_trie_with_leaf(&self, leaf_trie: AccessTrie) -> RootAccessTrie {
let mut current = leaf_trie;
for field in self.path.iter().rev() {
let mut fields = HashMap::new();
fields.insert(field.clone(), Box::new(current));
current = AccessTrie {
ancestors_trie: Default::default(),
is_ancestor: false,
children: fields,
node_type: None,
};
}
let mut primary_map = HashMap::new();
if current != AccessTrie::new() {
primary_map.insert(self.root.clone(), current);
}
RootAccessTrie { trie: primary_map }
}
}
impl RootAccessTrie {
pub fn trie(&self) -> &HashMap<EntityRoot, AccessTrie> {
&self.trie
}
}
impl RootAccessTrie {
pub fn new() -> Self {
Self {
trie: Default::default(),
}
}
}
impl RootAccessTrie {
pub fn union(mut self, other: Self) -> Self {
self.union_mut(other);
self
}
pub fn union_mut(&mut self, other: Self) {
for (key, value) in other.trie {
match self.trie.entry(key) {
Entry::Occupied(mut occupied) => {
occupied.get_mut().union_mut(value);
}
Entry::Vacant(vacant) => {
vacant.insert(value);
}
}
}
}
}
impl Default for RootAccessTrie {
fn default() -> Self {
Self::new()
}
}
impl AccessTrie {
pub fn union(mut self, other: Self) -> Self {
self.union_mut(other);
self
}
pub fn union_mut(&mut self, other: Self) {
union_fields_mut(&mut self.children, other.children);
self.ancestors_trie.union_mut(other.ancestors_trie);
self.is_ancestor = self.is_ancestor || other.is_ancestor;
}
pub fn children(&self) -> &Fields {
&self.children
}
pub fn ancestors_required(&self) -> &RootAccessTrie {
&self.ancestors_trie
}
}
impl AccessTrie {
pub(crate) fn new() -> Self {
Self {
children: Default::default(),
ancestors_trie: Default::default(),
is_ancestor: false,
node_type: None,
}
}
}
impl Default for AccessTrie {
fn default() -> Self {
Self::new()
}
}
pub fn compute_entity_manifest(
validator: &Validator,
policies: &PolicySet,
) -> Result<EntityManifest, EntityManifestError> {
let validation_res = validator.validate(policies, ValidationMode::Strict);
if !validation_res.validation_passed() {
return Err(EntityManifestError::Validation(validation_res));
}
let mut manifest: HashMap<RequestType, RootAccessTrie> = HashMap::new();
let typechecker = Typechecker::new(validator.schema(), ValidationMode::Strict);
for policy in policies.policies() {
let request_envs = typechecker.typecheck_by_request_env(policy.template());
for (request_env, policy_check) in request_envs {
let new_primary_slice = match policy_check {
PolicyCheck::Success(typechecked_expr) => {
entity_manifest_from_expr(&typechecked_expr).map(|val| val.global_trie)
}
PolicyCheck::Irrelevant(_, _) => {
Ok(RootAccessTrie::new())
}
#[expect(
clippy::panic,
reason = "policy check should not fail after full strict validation above"
)]
PolicyCheck::Fail(_errors) => {
panic!("Policy check failed after validation succeeded")
}
}?;
let request_type = request_env
.to_request_type()
.ok_or(PartialRequestError {})?;
match manifest.entry(request_type) {
Entry::Occupied(mut occupied) => {
occupied.get_mut().union_mut(new_primary_slice);
}
Entry::Vacant(vacant) => {
vacant.insert(new_primary_slice);
}
}
}
}
#[expect(
clippy::unwrap_used,
reason = "entity manifest cannot be out of date, since it was computed from the schema given"
)]
Ok(EntityManifest {
per_action: manifest,
}
.to_typed(validator.schema())
.unwrap())
}
fn entity_manifest_from_expr(
expr: &Expr<Option<Type>>,
) -> Result<EntityManifestAnalysisResult, EntityManifestError> {
match expr.expr_kind() {
ExprKind::Slot(slot_id) => {
if slot_id.is_principal() {
Ok(EntityManifestAnalysisResult::from_root(EntityRoot::Var(
Var::Principal,
)))
} else {
assert!(slot_id.is_resource());
Ok(EntityManifestAnalysisResult::from_root(EntityRoot::Var(
Var::Resource,
)))
}
}
ExprKind::Var(var) => Ok(EntityManifestAnalysisResult::from_root(EntityRoot::Var(
*var,
))),
ExprKind::Lit(Literal::EntityUID(literal)) => Ok(EntityManifestAnalysisResult::from_root(
EntityRoot::Literal((**literal).clone()),
)),
ExprKind::Unknown(_) => Err(PartialExpressionError {})?,
ExprKind::Lit(_) => Ok(EntityManifestAnalysisResult::default()),
ExprKind::If {
test_expr,
then_expr,
else_expr,
} => Ok(entity_manifest_from_expr(test_expr)?
.empty_paths()
.union(entity_manifest_from_expr(then_expr)?)
.union(entity_manifest_from_expr(else_expr)?)),
ExprKind::And { left, right }
| ExprKind::Or { left, right }
| ExprKind::BinaryApp {
op: BinaryOp::Less | BinaryOp::LessEq | BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul,
arg1: left,
arg2: right,
} => Ok(entity_manifest_from_expr(left)?
.empty_paths()
.union(entity_manifest_from_expr(right)?.empty_paths())),
ExprKind::UnaryApp { op, arg } => {
match op {
UnaryOp::Not | UnaryOp::Neg => Ok(entity_manifest_from_expr(arg)?.empty_paths()),
UnaryOp::IsEmpty => {
#[expect(
clippy::expect_used,
reason = "Typechecking succeeded, so type annotations are present"
)]
let ty = arg
.data()
.as_ref()
.expect("Expected annotated types after typechecking");
Ok(entity_manifest_from_expr(arg)?
.full_type_required(ty)
.empty_paths())
}
}
}
ExprKind::BinaryApp {
op:
op @ (BinaryOp::Eq
| BinaryOp::In
| BinaryOp::Contains
| BinaryOp::ContainsAll
| BinaryOp::ContainsAny),
arg1,
arg2,
} => {
let mut arg1_res = entity_manifest_from_expr(arg1)?;
let arg2_res = entity_manifest_from_expr(arg2)?;
#[expect(
clippy::expect_used,
reason = "Typechecking succeeded, so type annotations are present"
)]
let ty1 = arg1
.data()
.as_ref()
.expect("Expected annotated types after typechecking");
#[expect(
clippy::expect_used,
reason = "Typechecking succeeded, so type annotations are present"
)]
let ty2 = arg2
.data()
.as_ref()
.expect("Expected annotated types after typechecking");
if matches!(op, BinaryOp::In) {
arg1_res = arg1_res
.with_ancestors_required(&arg2_res.resulting_paths.to_ancestor_access_trie());
}
Ok(arg1_res
.full_type_required(ty1)
.union(arg2_res.full_type_required(ty2))
.empty_paths())
}
ExprKind::BinaryApp {
op: BinaryOp::GetTag | BinaryOp::HasTag,
arg1: _,
arg2: _,
} => Err(UnsupportedCedarFeatureError {
feature: "entity tags".into(),
}
.into()),
ExprKind::ExtensionFunctionApp { fn_name: _, args } => {
let mut res = EntityManifestAnalysisResult::default();
for arg in args.iter() {
res = res.union(entity_manifest_from_expr(arg)?);
}
Ok(res)
}
ExprKind::Like { expr, pattern: _ }
| ExprKind::Is {
expr,
entity_type: _,
} => {
Ok(entity_manifest_from_expr(expr)?.empty_paths())
}
ExprKind::Set(contents) => {
let mut res = EntityManifestAnalysisResult::default();
for expr in &**contents {
let content = entity_manifest_from_expr(expr)?;
res = res.union(content);
}
res.resulting_paths = WrappedAccessPaths::SetLiteral(Box::new(res.resulting_paths));
Ok(res)
}
ExprKind::Record(content) => {
let mut record_contents = HashMap::new();
let mut global_trie = RootAccessTrie::default();
for (key, child_expr) in content.iter() {
let res = entity_manifest_from_expr(child_expr)?;
record_contents.insert(key.clone(), Box::new(res.resulting_paths));
global_trie = global_trie.union(res.global_trie);
}
Ok(EntityManifestAnalysisResult {
resulting_paths: WrappedAccessPaths::RecordLiteral(record_contents),
global_trie,
})
}
ExprKind::GetAttr { expr, attr } => {
Ok(entity_manifest_from_expr(expr)?.get_or_has_attr(attr))
}
ExprKind::HasAttr { expr, attr } => Ok(entity_manifest_from_expr(expr)?
.get_or_has_attr(attr)
.empty_paths()),
#[cfg(feature = "tolerant-ast")]
ExprKind::Error { .. } => Err(EntityManifestError::UnsupportedCedarFeature(
UnsupportedCedarFeatureError {
feature: "No support for AST error nodes".into(),
},
)),
}
}
#[cfg(test)]
mod entity_slice_tests {
use crate::{ast::PolicyID, extensions::Extensions, parser::parse_policy};
use similar_asserts::assert_eq;
use super::*;
fn schema() -> ValidatorSchema {
ValidatorSchema::from_cedarschema_str(
"
entity User = {
name: String,
};
entity Document;
action Read appliesTo {
principal: [User],
resource: [Document]
};
",
Extensions::all_available(),
)
.unwrap()
.0
}
fn document_fields_schema() -> ValidatorSchema {
ValidatorSchema::from_cedarschema_str(
"
entity User = {
name: String,
};
entity Document = {
owner: User,
viewer: User,
};
action Read appliesTo {
principal: [User],
resource: [Document]
};
",
Extensions::all_available(),
)
.unwrap()
.0
}
#[test]
fn test_simple_entity_manifest() {
let mut pset = PolicySet::new();
let policy = parse_policy(
None,
r#"permit(principal, action, resource)
when {
principal.name == "John"
};"#,
)
.expect("should succeed");
pset.add(policy.into()).expect("should succeed");
let validator = Validator::new(schema());
let entity_manifest = compute_entity_manifest(&validator, &pset).expect("Should succeed");
let expected_rust = EntityManifest {
per_action: HashMap::from([(
RequestType {
principal: "User".parse().unwrap(),
resource: "Document".parse().unwrap(),
action: r#"Action::"Read""#.parse().unwrap(),
},
RootAccessTrie {
trie: HashMap::from([(
EntityRoot::Var(Var::Principal),
AccessTrie {
children: HashMap::from([(
SmolStr::new_static("name"),
Box::new(AccessTrie {
children: HashMap::new(),
ancestors_trie: RootAccessTrie::new(),
is_ancestor: false,
node_type: Some(Type::primitive_string()),
}),
)]),
ancestors_trie: RootAccessTrie::new(),
is_ancestor: false,
node_type: Some(Type::named_entity_reference("User".parse().unwrap())),
},
)]),
},
)]),
};
let expected = serde_json::json! ({
"perAction": [
[
{
"principal": "User",
"action": {
"ty": "Action",
"eid": "Read"
},
"resource": "Document"
},
{
"trie": [
[
{
"var": "principal"
},
{
"children": [
[
"name",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
]
}
]
]
});
let expected_manifest =
EntityManifest::from_json_value(expected, validator.schema()).unwrap();
assert_eq!(entity_manifest, expected_manifest);
assert_eq!(entity_manifest, expected_rust);
}
#[test]
fn test_empty_entity_manifest() {
let mut pset = PolicySet::new();
let policy =
parse_policy(None, "permit(principal, action, resource);").expect("should succeed");
pset.add(policy.into()).expect("should succeed");
let validator = Validator::new(schema());
let entity_manifest = compute_entity_manifest(&validator, &pset).expect("Should succeed");
let expected = serde_json::json!(
{
"perAction": [
[
{
"principal": "User",
"action": {
"ty": "Action",
"eid": "Read"
},
"resource": "Document"
},
{
"trie": [
]
}
]
]
});
let expected_manifest =
EntityManifest::from_json_value(expected, validator.schema()).unwrap();
assert_eq!(entity_manifest, expected_manifest);
}
#[test]
fn test_entity_manifest_ancestors_required() {
let mut pset = PolicySet::new();
let policy = parse_policy(
None,
"permit(principal, action, resource)
when {
principal in resource || principal.manager in resource
};",
)
.expect("should succeed");
pset.add(policy.into()).expect("should succeed");
let schema = ValidatorSchema::from_cedarschema_str(
"
entity User in [Document] = {
name: String,
manager: User
};
entity Document;
action Read appliesTo {
principal: [User],
resource: [Document]
};
",
Extensions::all_available(),
)
.unwrap()
.0;
let validator = Validator::new(schema);
let entity_manifest = compute_entity_manifest(&validator, &pset).expect("Should succeed");
let expected = serde_json::json!(
{
"perAction": [
[
{
"principal": "User",
"action": {
"ty": "Action",
"eid": "Read"
},
"resource": "Document"
},
{
"trie": [
[
{
"var": "principal"
},
{
"children": [
[
"manager",
{
"children": [],
"ancestorsTrie": {
"trie": [
[
{
"var": "resource",
},
{
"children": [],
"isAncestor": true,
"ancestorsTrie": { "trie": [] }
}
]
]
},
"isAncestor": false
}
]
],
"ancestorsTrie": {
"trie": [
[
{
"var": "resource",
},
{
"children": [],
"isAncestor": true,
"ancestorsTrie": { "trie": [] }
}
]
]
},
"isAncestor": false
}
]
]
}
]
]
});
let expected_manifest =
EntityManifest::from_json_value(expected, validator.schema()).unwrap();
assert_eq!(entity_manifest, expected_manifest);
}
#[test]
fn test_entity_manifest_multiple_types() {
let mut pset = PolicySet::new();
let policy = parse_policy(
None,
r#"permit(principal, action, resource)
when {
principal.name == "John"
};"#,
)
.expect("should succeed");
pset.add(policy.into()).expect("should succeed");
let schema = ValidatorSchema::from_cedarschema_str(
"
entity User = {
name: String,
};
entity OtherUserType = {
name: String,
irrelevant: String,
};
entity Document;
action Read appliesTo {
principal: [User, OtherUserType],
resource: [Document]
};
",
Extensions::all_available(),
)
.unwrap()
.0;
let validator = Validator::new(schema);
let entity_manifest = compute_entity_manifest(&validator, &pset).expect("Should succeed");
let expected = serde_json::json!(
{
"perAction": [
[
{
"principal": "User",
"action": {
"ty": "Action",
"eid": "Read"
},
"resource": "Document"
},
{
"trie": [
[
{
"var": "principal"
},
{
"children": [
[
"name",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
]
}
],
[
{
"principal": "OtherUserType",
"action": {
"ty": "Action",
"eid": "Read"
},
"resource": "Document"
},
{
"trie": [
[
{
"var": "principal"
},
{
"children": [
[
"name",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
]
}
]
]
});
let expected_manifest =
EntityManifest::from_json_value(expected, validator.schema()).unwrap();
assert_eq!(entity_manifest, expected_manifest);
}
#[test]
fn test_entity_manifest_multiple_branches() {
let mut pset = PolicySet::new();
let policy1 = parse_policy(
None,
r#"
permit(
principal,
action == Action::"Read",
resource
)
when
{
resource.readers.contains(principal)
};"#,
)
.unwrap();
let policy2 = parse_policy(
Some(PolicyID::from_string("Policy2")),
r#"permit(
principal,
action == Action::"Read",
resource
)
when
{
resource.metadata.owner == principal
};"#,
)
.unwrap();
pset.add(policy1.into()).expect("should succeed");
pset.add(policy2.into()).expect("should succeed");
let schema = ValidatorSchema::from_cedarschema_str(
"
entity User;
entity Metadata = {
owner: User,
time: String,
};
entity Document = {
metadata: Metadata,
readers: Set<User>,
};
action Read appliesTo {
principal: [User],
resource: [Document]
};
",
Extensions::all_available(),
)
.unwrap()
.0;
let validator = Validator::new(schema);
let entity_manifest = compute_entity_manifest(&validator, &pset).expect("Should succeed");
let expected = serde_json::json!(
{
"perAction": [
[
{
"principal": "User",
"action": {
"ty": "Action",
"eid": "Read"
},
"resource": "Document"
},
{
"trie": [
[
{
"var": "resource"
},
{
"children": [
[
"metadata",
{
"children": [
[
"owner",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
],
[
"readers",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
],
]
}
]
]
});
let expected_manifest =
EntityManifest::from_json_value(expected, validator.schema()).unwrap();
assert_eq!(entity_manifest, expected_manifest);
}
#[test]
fn test_entity_manifest_struct_equality() {
let mut pset = PolicySet::new();
let policy = parse_policy(
None,
r#"permit(principal, action, resource)
when {
principal.metadata.nickname == "timmy" && principal.metadata == {
"friends": [ "oliver" ],
"nickname": "timmy"
}
};"#,
)
.expect("should succeed");
pset.add(policy.into()).expect("should succeed");
let schema = ValidatorSchema::from_cedarschema_str(
"
entity User = {
name: String,
metadata: {
friends: Set<String>,
nickname: String,
},
};
entity Document;
action BeSad appliesTo {
principal: [User],
resource: [Document]
};
",
Extensions::all_available(),
)
.unwrap()
.0;
let validator = Validator::new(schema);
let entity_manifest = compute_entity_manifest(&validator, &pset).expect("Should succeed");
let expected = serde_json::json!(
{
"perAction": [
[
{
"principal": "User",
"action": {
"ty": "Action",
"eid": "BeSad"
},
"resource": "Document"
},
{
"trie": [
[
{
"var": "principal"
},
{
"children": [
[
"metadata",
{
"children": [
[
"nickname",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
],
[
"friends",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
]
}
]
]
});
let expected_manifest =
EntityManifest::from_json_value(expected, validator.schema()).unwrap();
assert_eq!(entity_manifest, expected_manifest);
}
#[test]
fn test_entity_manifest_struct_equality_left_right_different() {
let mut pset = PolicySet::new();
let policy = parse_policy(
None,
r#"permit(principal, action, resource)
when {
principal.metadata == resource.metadata
};"#,
)
.expect("should succeed");
pset.add(policy.into()).expect("should succeed");
let schema = ValidatorSchema::from_cedarschema_str(
"
entity User = {
name: String,
metadata: {
friends: Set<String>,
nickname: String,
},
};
entity Document;
action Hello appliesTo {
principal: [User],
resource: [User]
};
",
Extensions::all_available(),
)
.unwrap()
.0;
let validator = Validator::new(schema);
let entity_manifest = compute_entity_manifest(&validator, &pset).expect("Should succeed");
let expected = serde_json::json!(
{
"perAction": [
[
{
"principal": "User",
"action": {
"ty": "Action",
"eid": "Hello"
},
"resource": "User"
},
{
"trie": [
[
{
"var": "resource"
},
{
"children": [
[
"metadata",
{
"children": [
[
"friends",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
],
[
"nickname",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
],
[
{
"var": "principal"
},
{
"children": [
[
"metadata",
{
"children": [
[
"nickname",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
],
[
"friends",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
]
}
]
]
});
let expected_manifest =
EntityManifest::from_json_value(expected, validator.schema()).unwrap();
assert_eq!(entity_manifest, expected_manifest);
}
#[test]
fn test_entity_manifest_with_if() {
let mut pset = PolicySet::new();
let validator = Validator::new(document_fields_schema());
let policy = parse_policy(
None,
r#"permit(principal, action, resource)
when {
if principal.name == "John"
then resource.owner.name == User::"oliver".name
else resource.viewer == User::"oliver"
};"#,
)
.expect("should succeed");
pset.add(policy.into()).expect("should succeed");
let entity_manifest = compute_entity_manifest(&validator, &pset).expect("Should succeed");
let expected = serde_json::json! ( {
"perAction": [
[
{
"principal": "User",
"action": {
"ty": "Action",
"eid": "Read"
},
"resource": "Document"
},
{
"trie": [
[
{
"var": "principal"
},
{
"children": [
[
"name",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
],
[
{
"literal": {
"ty": "User",
"eid": "oliver"
}
},
{
"children": [
[
"name",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
],
[
{
"var": "resource"
},
{
"children": [
[
"viewer",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
],
[
"owner",
{
"children": [
[
"name",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
]
}
]
]
}
);
let expected_manifest =
EntityManifest::from_json_value(expected, validator.schema()).unwrap();
assert_eq!(entity_manifest, expected_manifest);
}
#[test]
fn test_entity_manifest_if_literal_record() {
let mut pset = PolicySet::new();
let validator = Validator::new(document_fields_schema());
let policy = parse_policy(
None,
r#"permit(principal, action, resource)
when {
{
"myfield":
{
"secondfield":
if principal.name == "yihong"
then principal
else resource.owner,
"ignored but still important due to errors":
resource.viewer
}
}["myfield"]["secondfield"].name == "pavel"
};"#,
)
.expect("should succeed");
pset.add(policy.into()).expect("should succeed");
let entity_manifest = compute_entity_manifest(&validator, &pset).expect("Should succeed");
let expected = serde_json::json! ( {
"perAction": [
[
{
"principal": "User",
"action": {
"ty": "Action",
"eid": "Read"
},
"resource": "Document"
},
{
"trie": [
[
{
"var": "principal"
},
{
"children": [
[
"name",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
],
[
{
"var": "resource"
},
{
"children": [
[
"viewer",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
],
[
"owner",
{
"children": [
[
"name",
{
"children": [],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
],
"ancestorsTrie": { "trie": []},
"isAncestor": false
}
]
]
}
]
]
}
);
let expected_manifest =
EntityManifest::from_json_value(expected, validator.schema()).unwrap();
assert_eq!(entity_manifest, expected_manifest);
}
#[test]
fn test_entity_manifest_action_in() {
let mut pset = PolicySet::new();
let policy = parse_policy(
None,
r#"permit(principal, action in Action::"parent", resource);"#,
)
.unwrap();
pset.add(policy.into()).unwrap();
let schema = ValidatorSchema::from_cedarschema_str(
"entity e; action action in parent appliesTo { principal: e, resource: e}; action parent;",
Extensions::all_available(),
)
.unwrap()
.0;
let validator = Validator::new(schema);
let entity_manifest = compute_entity_manifest(&validator, &pset).expect("Should succeed");
let expected = EntityManifest::from_json_value(
serde_json::json!({
"perAction": [
[
{
"principal": "e",
"action": {
"ty": "Action",
"eid": "action"
},
"resource": "e"
},
{
"trie": [
[
{
"var": "action"
},
{
"children": [ ],
"ancestorsTrie": {
"trie": [
[
{ "literal": {"ty": "Action", "eid": "parent"} },
{
"children": [],
"isAncestor": true,
"ancestorsTrie": { "trie": [] }
}
]
]
},
"isAncestor": false
}
],
]
}
]
]
}),
validator.schema(),
)
.unwrap();
assert_eq!(entity_manifest, expected);
}
}