diff --git a/cedar-policy-core/src/ast/entity.rs b/cedar-policy-core/src/ast/entity.rs index 1408748fb9..38be60363c 100644 --- a/cedar-policy-core/src/ast/entity.rs +++ b/cedar-policy-core/src/ast/entity.rs @@ -318,6 +318,49 @@ pub struct Entity { } impl Entity { + /// The implementation of [`Eq`] and [`PartialEq`] for + /// entities just compares entity ids. + /// This implementation does a more traditional, deep equality + /// check comparing attributes, ancestors, and the id. + pub fn deep_equal(&self, other: &Self) -> bool { + self.uid == other.uid && self.attrs == other.attrs && self.ancestors == other.ancestors + } + + /// Union two compatible entities, creating a new entity + /// with atributes from both. + /// The union is deep, meaning that if both entities have + /// records these records get unioned. + /// Returns `None` when incompatible. + pub fn union(&self, other: &Self) -> Option { + if self.uid() != other.uid() { + return None; + } + + let mut new_attrs: HashMap = self + .attrs + .iter() + .map(|item| (item.0.clone(), item.1.as_ref().clone())) + .collect(); + for (key, val) in &other.attrs { + if let Some(v) = new_attrs.get_mut(key) { + *v = v.union(val.as_ref())?; + } else { + new_attrs.insert(key.clone(), val.as_ref().clone()); + } + } + + let mut new_ancestors = self.ancestors.clone(); + for ancestor in &other.ancestors { + new_ancestors.insert(ancestor.clone()); + } + + Some(Entity::new_with_attr_partial_value( + self.uid().clone(), + new_attrs, + new_ancestors, + )) + } + /// Create a new `Entity` with this UID, attributes, and ancestors pub fn new( uid: EntityUID, diff --git a/cedar-policy-core/src/ast/partial_value.rs b/cedar-policy-core/src/ast/partial_value.rs index 86bf119c96..8d0339816f 100644 --- a/cedar-policy-core/src/ast/partial_value.rs +++ b/cedar-policy-core/src/ast/partial_value.rs @@ -31,6 +31,24 @@ pub enum PartialValue { } impl PartialValue { + /// Union two partial values, combining fields of records. + /// When two partial values are incompatible, returns `None`. + /// When two partial values are both partial, returns `None`. + pub fn union(&self, other: &Self) -> Option { + match (self, other) { + (PartialValue::Value(v1), PartialValue::Value(v2)) => { + Some(PartialValue::Value(v1.union(v2)?)) + } + (PartialValue::Value(v1), PartialValue::Residual(_)) => { + Some(PartialValue::Value(v1.clone())) + } + (PartialValue::Residual(_), PartialValue::Value(v1)) => { + Some(PartialValue::Value(v1.clone())) + } + (PartialValue::Residual(_r1), PartialValue::Residual(_r2)) => None, + } + } + /// Create a new `PartialValue` consisting of just this single `Unknown` pub fn unknown(u: Unknown) -> Self { Self::Residual(Expr::unknown(u)) diff --git a/cedar-policy-core/src/ast/value.rs b/cedar-policy-core/src/ast/value.rs index 7f81c4e262..72835e5aed 100644 --- a/cedar-policy-core/src/ast/value.rs +++ b/cedar-policy-core/src/ast/value.rs @@ -68,6 +68,35 @@ impl PartialOrd for Value { } impl Value { + /// Unions two compatible [`Value`]s, combining fields + /// for records. + /// When two values are incompatible, returns `None`. + pub fn union(&self, other: &Self) -> Option { + match (self.value_kind(), other.value_kind()) { + (ValueKind::Record(r1), ValueKind::Record(r2)) => { + let mut new_map = (**r1).clone(); + for (field, val) in r2.iter() { + if let Some(v) = new_map.get_mut(field) { + *v = v.union(val)?; + } else { + new_map.insert(field.clone(), val.clone()); + } + } + Some(Value::new( + ValueKind::Record(Arc::new(new_map)), + self.source_loc().cloned().or(other.source_loc().cloned()), + )) + } + _ => { + if self == other { + Some(self.clone()) + } else { + None + } + } + } + } + /// Create a new empty set pub fn empty_set(loc: Option) -> Self { Self { diff --git a/cedar-policy-core/src/entities.rs b/cedar-policy-core/src/entities.rs index a8a3cfe32f..bb733f8c97 100644 --- a/cedar-policy-core/src/entities.rs +++ b/cedar-policy-core/src/entities.rs @@ -72,6 +72,34 @@ pub struct Entities { } impl Entities { + /// The implementation of [`Eq`] and [`PartialEq`] on [`Entities`] + /// only checks equality by id for entities in the store. + /// This method checks that the entities are equal deeply, + /// using `[Entity::deep_equal]` to check equality. + pub fn deep_equal(&self, other: &Self) -> bool { + if self.mode != other.mode { + false + } else { + for (key, value) in &self.entities { + if let Some(other_value) = other.entities.get(key) { + if !value.deep_equal(other_value) { + return false; + } + } else { + return false; + } + } + + for key in other.entities.keys() { + if !self.entities.contains_key(key) { + return false; + } + } + + true + } + } + /// Create a fresh `Entities` with no entities pub fn new() -> Self { Self { diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index d726365554..a25a0598a5 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -59,7 +59,7 @@ where /// A map from request types to [`RootAccessTrie`]s. #[serde_as(as = "Vec<(_, _)>")] #[serde(bound(deserialize = "T: Default"))] - per_action: HashMap>, + pub(crate) per_action: HashMap>, } /// A map of data fields to [`AccessTrie`]s. @@ -117,7 +117,7 @@ where /// The data that needs to be loaded, organized by root. #[serde_as(as = "Vec<(_, _)>")] #[serde(bound(deserialize = "T: Default"))] - trie: HashMap>, + pub(crate) trie: HashMap>, } /// A Trie representing a set of data paths to load, @@ -137,15 +137,15 @@ pub struct AccessTrie { /// Child data of this entity slice. /// The keys are edges in the trie pointing to sub-trie values. #[serde_as(as = "Vec<(_, _)>")] - children: Fields, + pub(crate) children: Fields, /// For entity types, this boolean may be `true` /// to signal that all the ancestors in the entity hierarchy /// are required (transitively). - ancestors_required: bool, + pub(crate) ancestors_required: bool, /// Optional data annotation, usually used for type information. #[serde(skip_serializing, skip_deserializing)] #[serde(bound(deserialize = "T: Default"))] - data: T, + pub(crate) data: T, } /// A data path that may end with requesting the parents of @@ -342,7 +342,7 @@ impl Default for RootAccessTrie { impl AccessTrie { /// Union two [`AccessTrie`]s together. /// The new trie requests the data from both of the original. - fn union(&self, other: &Self) -> Self { + pub fn union(&self, other: &Self) -> Self { Self { children: union_fields(&self.children, &other.children), ancestors_required: self.ancestors_required || other.ancestors_required, @@ -370,7 +370,7 @@ impl AccessTrie { impl AccessTrie { /// A new trie that requests no data. - fn new() -> Self { + pub fn new() -> Self { Self { children: Default::default(), ancestors_required: false, diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs new file mode 100644 index 0000000000..046a039407 --- /dev/null +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -0,0 +1,760 @@ +/* + * Copyright Cedar Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Entity Slicing + +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::fmt::Display; + +use cedar_policy_core::entities::err::EntitiesError; +use cedar_policy_core::entities::{Dereference, NoEntitiesSchema, TCComputation}; +use cedar_policy_core::extensions::Extensions; +use cedar_policy_core::{ + ast::{Entity, EntityUID, Literal, PartialValue, Request, Value, ValueKind, Var}, + entities::Entities, +}; +use miette::Diagnostic; +use smol_str::SmolStr; +use thiserror::Error; + +use crate::entity_manifest::{ + AccessTrie, EntityManifest, EntityRoot, PartialRequestError, RootAccessTrie, +}; + +/// Error when expressions are partial during entity +/// slicing. +// CAUTION: this type is publicly exported in `cedar-policy`. +// Don't make fields `pub`, don't make breaking changes, and use caution +// when adding public methods. +#[derive(Debug, Clone, Error, Eq, PartialEq)] +#[error( + "Entity slicing requires fully concrete policies. Got a policy with an unknown expression." +)] +pub struct PartialExpressionError {} + +impl Diagnostic for PartialExpressionError {} + +/// Error when expressions are partial during entity +/// slicing. +// CAUTION: this type is publicly exported in `cedar-policy`. +// Don't make fields `pub`, don't make breaking changes, and use caution +// when adding public methods. +#[derive(Debug, Clone, Error, Eq, PartialEq)] +#[error( + "Entity slicing requires fully concrete policies. Got a policy with an unknown expression." +)] +pub struct IncompatibleEntityManifestError { + non_record_entity_value: Value, +} + +impl Diagnostic for IncompatibleEntityManifestError { + fn help<'a>(&'a self) -> Option> { + Some(Box::new(format!( + "Expected entity or record during entity loading. Got value: {}", + self.non_record_entity_value + ))) + } +} + +/// Error when entities are partial during entity manifest computation. +// CAUTION: this type is publicly exported in `cedar-policy`. +// Don't make fields `pub`, don't make breaking changes, and use caution +// when adding public methods. +#[derive(Debug, Clone, Error, Eq, PartialEq)] +#[error("Entity slicing requires fully concrete entities. Got a partial entity.")] +pub struct PartialEntityError {} + +impl Diagnostic for PartialEntityError {} + +/// An error generated by entity slicing. +/// TODO make public API wrapper +#[derive(Debug, Error, Diagnostic)] +pub enum EntitySliceError { + /// An entities error was encountered + #[error(transparent)] + #[diagnostic(transparent)] + Entities(#[from] EntitiesError), + + /// The request was partial + #[error(transparent)] + PartialRequest(#[from] PartialRequestError), + /// A policy was partial + #[error(transparent)] + PartialExpression(#[from] PartialExpressionError), + + /// During entity loading, attempted to load from + /// a type without fields. + #[error(transparent)] + IncompatibleEntityManifest(#[from] IncompatibleEntityManifestError), + + /// Found a partial entity during entity loading. + #[error(transparent)] + PartialEntity(#[from] PartialEntityError), +} + +impl EntityManifest { + /// Use this entity manifest to + /// find an entity slice using an existing [`Entities`] store. + pub fn slice_entities( + &self, + entities: &Entities, + request: &Request, + ) -> Result { + let request_type = request.to_request_type().ok_or(PartialRequestError {})?; + self.per_action + .get(&request_type) + .map(|primary| primary.slice_entities(entities, request)) + .unwrap_or(Ok(Entities::default())) + } +} + +impl RootAccessTrie { + /// Given entities and a request, return a new entitity store + /// which is a slice of the old one. + fn slice_entities( + &self, + entities: &Entities, + request: &Request, + ) -> Result { + let mut res = HashMap::::new(); + for (root, slice) in &self.trie { + match root { + EntityRoot::Literal(lit) => { + slice.slice_entity(entities, lit, &mut res)?; + } + EntityRoot::Var(Var::Action) => { + let entity_id = request.action().uid().ok_or(PartialRequestError {})?; + slice.slice_entity(entities, entity_id, &mut res)?; + } + EntityRoot::Var(Var::Principal) => { + let entity_id = request.principal().uid().ok_or(PartialRequestError {})?; + slice.slice_entity(entities, entity_id, &mut res)?; + } + EntityRoot::Var(Var::Resource) => { + let resource_id = request.resource().uid().ok_or(PartialRequestError {})?; + slice.slice_entity(entities, resource_id, &mut res)?; + } + EntityRoot::Var(Var::Context) => { + if slice.children.is_empty() { + // no data loading needed + } else { + let partial_val: PartialValue = PartialValue::from( + request.context().ok_or(PartialRequestError {})?.clone(), + ); + let PartialValue::Value(val) = partial_val else { + return Err(PartialRequestError {}.into()); + }; + slice.slice_val(entities, &val, &mut res)?; + } + } + } + } + Ok(Entities::from_entities( + res.into_values(), + None::<&NoEntitiesSchema>, + TCComputation::AssumeAlreadyComputed, + Extensions::all_available(), + )?) + } +} + +impl AccessTrie { + /// Given an entities store, an entity id, and a resulting store + /// Slice the entities and put them in the resulting store. + fn slice_entity( + &self, + entities: &Entities, + lit: &EntityUID, + res: &mut HashMap, + ) -> Result<(), EntitySliceError> { + // If the entity is not present, no need to slice + let Dereference::Data(entity) = entities.entity(lit) else { + return Ok(()); + }; + let mut new_entity = HashMap::::new(); + for (field, slice) in &self.children { + // only slice when field is available + if let Some(pval) = entity.get(field).cloned() { + let PartialValue::Value(val) = pval else { + return Err(PartialEntityError {}.into()); + }; + let sliced = slice.slice_val(entities, &val, res)?; + + new_entity.insert(field.clone(), PartialValue::Value(sliced)); + } + } + + let new_ancestors = if self.ancestors_required { + entity.ancestors().cloned().collect() + } else { + HashSet::new() + }; + + let new_entity = + Entity::new_with_attr_partial_value(lit.clone(), new_entity, new_ancestors); + + // PANIC SAFETY: Entities in the entity store with the same ID should be compatible to union together. + #[allow(clippy::expect_used)] + if let Some(existing) = res.get_mut(lit) { + // Here we union the new entity with any existing one + *existing = existing + .union(&new_entity) + .expect("Incompatible values found in entity store"); + } else { + res.insert(lit.clone(), new_entity); + } + Ok(()) + } + + fn slice_val( + &self, + entities: &Entities, + val: &Value, + res: &mut HashMap, + ) -> Result { + // unless this is an entity id, parents should not be required + assert!( + !self.ancestors_required + || matches!(val.value_kind(), ValueKind::Lit(Literal::EntityUID(_))) + ); + + Ok(match val.value_kind() { + ValueKind::Lit(Literal::EntityUID(id)) => { + self.slice_entity(entities, id, res)?; + val.clone() + } + ValueKind::Set(_) | ValueKind::ExtensionValue(_) | ValueKind::Lit(_) => { + if !self.children.is_empty() { + return Err(IncompatibleEntityManifestError { + non_record_entity_value: val.clone(), + } + .into()); + } + + val.clone() + } + ValueKind::Record(record) => { + let mut new_map = BTreeMap::::new(); + for (field, slice) in &self.children { + // only slice when field is available + if let Some(v) = record.get(field) { + new_map.insert(field.clone(), slice.slice_val(entities, v, res)?); + } + } + + Value::new(ValueKind::record(new_map), None) + } + }) + } +} + +#[cfg(test)] +mod entity_slice_tests { + use cedar_policy_core::{ + ast::{Context, PolicyID, PolicySet}, + entities::EntityJsonParser, + parser::parse_policy, + }; + + use crate::{entity_manifest::compute_entity_manifest, CoreSchema, ValidatorSchema}; + + use super::*; + + // Schema for testing in this module + 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 expect_entity_slice_to( + original: serde_json::Value, + expected: serde_json::Value, + schema: &ValidatorSchema, + manifest: &EntityManifest, + ) { + let request = Request::new( + ( + EntityUID::with_eid_and_type("User", "oliver").unwrap(), + None, + ), + ( + EntityUID::with_eid_and_type("Action", "Read").unwrap(), + None, + ), + ( + EntityUID::with_eid_and_type("Document", "dummy").unwrap(), + None, + ), + Context::empty(), + Some(schema), + Extensions::all_available(), + ) + .unwrap(); + + let schema = CoreSchema::new(schema); + let parser: EntityJsonParser<'_, '_, CoreSchema<'_>> = EntityJsonParser::new( + Some(&schema), + Extensions::all_available(), + TCComputation::AssumeAlreadyComputed, + ); + let original_entities = parser.from_json_value(original).unwrap(); + + // Entity slicing results in invalid entity stores + // since attributes may be missing. + let parser_without_validation: EntityJsonParser<'_, '_> = EntityJsonParser::new( + None, + Extensions::all_available(), + TCComputation::AssumeAlreadyComputed, + ); + let expected_entities = parser_without_validation.from_json_value(expected).unwrap(); + + let sliced_entities = manifest + .slice_entities(&original_entities, &request) + .unwrap(); + + // PANIC SAFETY: panic in testing when test fails + #[allow(clippy::panic)] + if !sliced_entities.deep_equal(&expected_entities) { + panic!( + "Sliced entities differed from expected. Expected:\n{}\nGot:\n{}", + expected_entities.to_json_value().unwrap(), + sliced_entities.to_json_value().unwrap() + ); + } + } + + #[test] + fn test_simple_entity_manifest() { + let mut pset = PolicySet::new(); + let policy = parse_policy( + None, + "permit(principal, action, resource) +when { + principal.name == \"John\" +};", + ) + .expect("should succeed"); + pset.add(policy.into()).expect("should succeed"); + + let schema = schema(); + + let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); + + let entities_json = serde_json::json!( + [ + { + "uid" : { "type" : "User", "id" : "oliver"}, + "attrs" : { + "name" : "Oliver" + }, + "parents" : [] + }, + { + "uid" : { "type" : "User", "id" : "oliver2"}, + "attrs" : { + "name" : "Oliver2" + }, + "parents" : [] + }, + ] + ); + + let expected_entities_json = serde_json::json!( + [ + { + "uid" : { "type" : "User", "id" : "oliver"}, + "attrs" : { + "name" : "Oliver" + }, + "parents" : [] + }, + ] + ); + + expect_entity_slice_to( + entities_json, + expected_entities_json, + &schema, + &entity_manifest, + ); + } + + #[test] + #[should_panic(expected = "Sliced entities differed")] + fn sanity_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 schema = schema(); + + let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); + + let entities_json = serde_json::json!( + [ + { + "uid" : { "type" : "User", "id" : "oliver"}, + "attrs" : { + "name" : "Oliver" + }, + "parents" : [] + }, + { + "uid" : { "type" : "User", "id" : "oliver2"}, + "attrs" : { + "name" : "Oliver2" + }, + "parents" : [] + }, + ] + ); + + let expected_entities_json = serde_json::json!([ + { + "uid" : { "type" : "User", "id" : "oliver"}, + "attrs" : { + "name" : "Oliver" + }, + "parents" : [] + }, + { + "uid" : { "type" : "User", "id" : "oliver2"}, + "attrs" : { + "name" : "Oliver2" + }, + "parents" : [] + }, + ]); + + expect_entity_slice_to( + entities_json, + expected_entities_json, + &schema, + &entity_manifest, + ); + } + + #[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 schema = schema(); + + let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); + + let entities_json = serde_json::json!( + [ + { + "uid" : { "type" : "User", "id" : "oliver"}, + "attrs" : { + "name" : "Oliver" + }, + "parents" : [] + }, + { + "uid" : { "type" : "User", "id" : "oliver2"}, + "attrs" : { + "name" : "Oliver2" + }, + "parents" : [] + }, + ] + ); + + let expected_entities_json = serde_json::json!([]); + + expect_entity_slice_to( + entities_json, + expected_entities_json, + &schema, + &entity_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 entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); + + let entities_json = serde_json::json!( + [ + { + "uid" : { "type" : "User", "id" : "oliver"}, + "attrs" : { + "name" : "Oliver", + "manager": { "type" : "User", "id" : "george"} + }, + "parents" : [ + { "type" : "Document", "id" : "oliverdocument"} + ] + }, + { + "uid" : { "type" : "User", "id" : "george"}, + "attrs" : { + "name" : "George", + "manager": { "type" : "User", "id" : "george"} + }, + "parents" : [ + { "type" : "Document", "id" : "georgedocument"} + ] + }, + ] + ); + + let expected_entities_json = serde_json::json!( + [ + { + "uid" : { "type" : "User", "id" : "oliver"}, + "attrs" : { + "manager": { "__entity": { "type" : "User", "id" : "george"} } + }, + "parents" : [ + { "type" : "Document", "id" : "oliverdocument"} + ] + }, + { + "uid" : { "type" : "User", "id" : "george"}, + "attrs" : { + }, + "parents" : [ + { "type" : "Document", "id" : "georgedocument"} + ] + }, + ] + ); + + expect_entity_slice_to( + entities_json, + expected_entities_json, + &schema, + &entity_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, +}; + +action Read appliesTo { + principal: [User], + resource: [Document] +}; + ", + Extensions::all_available(), + ) + .unwrap() + .0; + + let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); + + let entities_json = serde_json::json!( + [ + { + "uid" : { "type" : "User", "id" : "oliver"}, + "attrs" : { + }, + "parents" : [ + ] + }, + { + "uid": { "type": "Document", "id": "dummy"}, + "attrs": { + "metadata": { "type": "Metadata", "id": "olivermetadata"}, + "readers": [{"type": "User", "id": "oliver"}] + }, + "parents": [], + }, + { + "uid": { "type": "Metadata", "id": "olivermetadata"}, + "attrs": { + "owner": { "type": "User", "id": "oliver"}, + "time": "now" + }, + "parents": [], + }, + ] + ); + + let expected_entities_json = serde_json::json!( + [ + { + "uid": { "type": "Document", "id": "dummy"}, + "attrs": { + "metadata": {"__entity": { "type": "Metadata", "id": "olivermetadata"}}, + "readers": [{ "__entity": {"type": "User", "id": "oliver"}}] + }, + "parents": [], + }, + { + "uid": { "type": "Metadata", "id": "olivermetadata"}, + "attrs": { + "owner": {"__entity": { "type": "User", "id": "oliver"}}, + }, + "parents": [], + }, + { + "uid" : { "type" : "User", "id" : "oliver"}, + "attrs" : { + }, + "parents" : [ + ] + }, + ] + ); + + expect_entity_slice_to( + entities_json, + expected_entities_json, + &schema, + &entity_manifest, + ); + } + + #[test] + fn test_entity_manifest_struct_equality() { + let mut pset = PolicySet::new(); + // we need to load all of the metadata, not just nickname + // no need to load actual name + 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, + nickname: String, + }, +}; + +entity Document; + +action BeSad appliesTo { + principal: [User], + resource: [Document] +}; + ", + Extensions::all_available(), + ) + .unwrap() + .0; + + let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); + assert_eq!(entity_manifest, entity_manifest); + } +} diff --git a/cedar-policy-validator/src/lib.rs b/cedar-policy-validator/src/lib.rs index ef08a80db1..66b1337753 100644 --- a/cedar-policy-validator/src/lib.rs +++ b/cedar-policy-validator/src/lib.rs @@ -37,6 +37,8 @@ use std::collections::HashSet; #[cfg(feature = "entity-manifest")] pub mod entity_manifest; +#[cfg(feature = "entity-manifest")] +pub mod entity_slicing; mod err; pub use err::*; mod coreschema; diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index aecc0f3ed6..5a36929729 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -38,9 +38,9 @@ pub use err::*; pub use ast::Effect; pub use authorizer::Decision; -use cedar_policy_core::ast; #[cfg(feature = "partial-eval")] use cedar_policy_core::ast::BorrowedRestrictedExpr; +use cedar_policy_core::ast::{self}; use cedar_policy_core::authorizer; use cedar_policy_core::entities::{ContextSchema, Dereference}; use cedar_policy_core::est::{self, TemplateLink}; @@ -4290,5 +4290,5 @@ pub fn compute_entity_manifest( schema: &Schema, pset: &PolicySet, ) -> Result { - entity_manifest::compute_entity_manifest(&schema.0, &pset.ast).map_err(|e| e.into()) + entity_manifest::compute_entity_manifest(&schema.0, &pset.ast).map_err(std::convert::Into::into) } diff --git a/cedar-policy/src/api/err.rs b/cedar-policy/src/api/err.rs index 42f7571af7..025a2b9a15 100644 --- a/cedar-policy/src/api/err.rs +++ b/cedar-policy/src/api/err.rs @@ -33,6 +33,8 @@ pub use cedar_policy_validator::cedar_schema::{schema_warnings, SchemaWarning}; use cedar_policy_validator::entity_manifest::{ self, FailedAnalysisError, PartialExpressionError, PartialRequestError, }; +#[cfg(feature = "entity-manifest")] +pub use cedar_policy_validator::entity_slicing::EntitySliceError; pub use cedar_policy_validator::{schema_errors, SchemaError}; use miette::Diagnostic; use ref_cast::RefCast;