From aa1f59e1a8a8ef4065aae4a7c818b692dcc8bb14 Mon Sep 17 00:00:00 2001 From: oflatt Date: Fri, 21 Jun 2024 16:57:05 -0700 Subject: [PATCH 01/56] Entity slicing implementation Signed-off-by: oflatt --- Cargo.toml | 2 +- cedar-policy-cli/src/lib.rs | 62 + cedar-policy-cli/src/main.rs | 1 + cedar-policy-core/Cargo.toml | 2 + cedar-policy-core/src/ast/entity.rs | 43 + cedar-policy-core/src/ast/partial_value.rs | 14 + cedar-policy-core/src/ast/request.rs | 44 +- cedar-policy-core/src/ast/value.rs | 29 + cedar-policy-core/src/entities.rs | 28 + cedar-policy-validator/Cargo.toml | 2 + cedar-policy-validator/src/entity_slicing.rs | 1882 +++++++++++++++++ cedar-policy-validator/src/err.rs | 4 + cedar-policy-validator/src/lib.rs | 1 + cedar-policy-validator/src/typecheck.rs | 32 +- .../src/typecheck/test/test_utils.rs | 30 - cedar-policy-validator/src/types.rs | 16 +- .../src/types/request_env.rs | 25 +- cedar-policy/Cargo.toml | 1 + cedar-policy/src/api.rs | 53 +- cedar-testing/src/cedar_test_impl.rs | 15 + cedar-testing/src/integration_testing.rs | 2 +- 21 files changed, 2241 insertions(+), 47 deletions(-) create mode 100644 cedar-policy-validator/src/entity_slicing.rs diff --git a/Cargo.toml b/Cargo.toml index 97627ff61a..3d8b991ced 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ "cedar-policy-formatter", "cedar-policy-cli", "cedar-testing", - "cedar-wasm" + "cedar-wasm", ] resolver = "2" diff --git a/cedar-policy-cli/src/lib.rs b/cedar-policy-cli/src/lib.rs index cebe29decd..98ec167989 100644 --- a/cedar-policy-cli/src/lib.rs +++ b/cedar-policy-cli/src/lib.rs @@ -89,6 +89,8 @@ pub enum Commands { Evaluate(EvaluateArgs), /// Validate a policy set against a schema Validate(ValidateArgs), + /// Compute the entity manifest for a schema and policy set + EntityManifest(EntityManifestArgs), /// Check that policies successfully parse CheckParse(CheckParseArgs), /// Link a template @@ -154,6 +156,14 @@ pub enum SchemaFormat { Json, } +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum ManifestFormat { + /// Human-readable format + Human, + /// JSON format + Json, +} + impl Default for SchemaFormat { fn default() -> Self { Self::Cedar @@ -170,6 +180,22 @@ pub enum ValidationMode { Partial, } +#[derive(Args, Debug)] +pub struct EntityManifestArgs { + /// File containing the schema + #[arg(short, long = "schema", value_name = "FILE")] + pub schema_file: String, + /// Policies args (incorporated by reference) + #[command(flatten)] + pub policies: PoliciesArgs, + /// Schema format (Human-readable or JSON) + #[arg(long, value_enum, default_value_t = SchemaFormat::Human)] + pub schema_format: SchemaFormat, + #[arg(long, value_enum, default_value_t = ManifestFormat::Human)] + /// Manifest format (Human-readable or JSON) + pub manifest_format: ManifestFormat, +} + #[derive(Args, Debug)] pub struct ValidateArgs { /// File containing the schema @@ -697,6 +723,42 @@ pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode { } } +pub fn entity_manifest(args: &EntityManifestArgs) -> CedarExitCode { + let pset = match args.policies.get_policy_set() { + Ok(pset) => pset, + Err(e) => { + println!("{e:?}"); + return CedarExitCode::Failure; + } + }; + + let schema = match read_schema_file(&args.schema_file, args.schema_format) { + Ok(schema) => schema, + Err(e) => { + println!("{e:?}"); + return CedarExitCode::Failure; + } + }; + + let manifest = match compute_entity_manifest(&schema, &pset) { + Ok(manifest) => manifest, + Err(e) => { + println!("{e:?}"); + return CedarExitCode::Failure; + } + }; + match args.manifest_format { + ManifestFormat::Human => { + println!("{}", manifest.to_str_natural()); + } + ManifestFormat::Json => { + println!("{}", serde_json::to_string_pretty(&manifest).unwrap()); + } + } + + CedarExitCode::Success +} + pub fn validate(args: &ValidateArgs) -> CedarExitCode { let mode = match args.validation_mode { ValidationMode::Strict => cedar_policy::ValidationMode::Strict, diff --git a/cedar-policy-cli/src/main.rs b/cedar-policy-cli/src/main.rs index 3772e634d1..3d8948e72f 100644 --- a/cedar-policy-cli/src/main.rs +++ b/cedar-policy-cli/src/main.rs @@ -46,6 +46,7 @@ fn main() -> CedarExitCode { Commands::Evaluate(args) => evaluate(&args).0, Commands::CheckParse(args) => check_parse(&args), Commands::Validate(args) => validate(&args), + Commands::EntityManifest(args) => entity_manifest(&args), Commands::Format(args) => format_policies(&args), Commands::Link(args) => link(&args), Commands::TranslatePolicy(args) => translate_policy(&args), diff --git a/cedar-policy-core/Cargo.toml b/cedar-policy-core/Cargo.toml index 390dff3820..aec811de08 100644 --- a/cedar-policy-core/Cargo.toml +++ b/cedar-policy-core/Cargo.toml @@ -28,6 +28,8 @@ stacker = "0.1.15" arbitrary = { version = "1", features = ["derive"], optional = true } miette = { version = "7.1.0", features = ["serde"] } nonempty = "0.10.0" +aws-sdk-dynamodb = "1.32.0" +futures = "0.3.1" # decimal extension requires regex regex = { version = "1.8", features = ["unicode"], optional = true } 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..23408d556f 100644 --- a/cedar-policy-core/src/ast/partial_value.rs +++ b/cedar-policy-core/src/ast/partial_value.rs @@ -31,6 +31,20 @@ 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/request.rs b/cedar-policy-core/src/ast/request.rs index e7fceb1862..e0c6af6b70 100644 --- a/cedar-policy-core/src/ast/request.rs +++ b/cedar-policy-core/src/ast/request.rs @@ -21,15 +21,15 @@ use crate::evaluator::{EvaluationError, RestrictedEvaluator}; use crate::extensions::Extensions; use crate::parser::Loc; use miette::Diagnostic; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use smol_str::SmolStr; use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use thiserror::Error; use super::{ - BorrowedRestrictedExpr, EntityUID, Expr, ExprKind, ExpressionConstructionError, PartialValue, - RestrictedExpr, Unknown, Value, ValueKind, Var, + BorrowedRestrictedExpr, EntityType, EntityUID, Expr, ExprKind, ExpressionConstructionError, + PartialValue, RestrictedExpr, Unknown, Value, ValueKind, Var, }; /// Represents the request tuple (see the Cedar design doc). @@ -49,6 +49,32 @@ pub struct Request { pub(crate) context: Option, } +/// Represents the principal, action, and resource types. +/// Used to index the [`EntityManifest`] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub struct RequestType { + /// Principal type + pub principal: EntityType, + /// Aciton type + pub action: EntityUID, + /// Resource type + pub resource: EntityType, +} + +impl RequestType { + /// Create a human-readable string for the request types, + /// written as a tuple of three elements. + /// TODO would be better as a cedar struct? + pub fn to_str_natural(&self) -> String { + format!( + "({} {} {})", + self.principal.name(), + self.action, + self.resource.name(), + ) + } +} + /// An entry in a request for a Entity UID. /// It may either be a concrete EUID /// or an unknown in the case of partial evaluation @@ -186,6 +212,18 @@ impl Request { pub fn context(&self) -> Option<&Context> { self.context.as_ref() } + + /// Get the request types that correspond to this request. + /// This includes the types of the principal, action, resource, + /// and context. + /// Returns `None` if the request is not fully concrete. + pub fn to_concrete_env(&self) -> Option { + Some(RequestType { + principal: self.principal.uid()?.clone().components().0, + action: self.action.uid()?.clone(), + resource: self.resource().uid()?.clone().components().0, + }) + } } impl std::fmt::Display for Request { 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/Cargo.toml b/cedar-policy-validator/Cargo.toml index a8f3f00b95..4b6667ff3d 100644 --- a/cedar-policy-validator/Cargo.toml +++ b/cedar-policy-validator/Cargo.toml @@ -27,6 +27,8 @@ arbitrary = { version = "1", features = ["derive"], optional = true } lalrpop-util = { version = "0.20.0", features = ["lexer", "unicode"] } lazy_static = "1.4.0" nonempty = "0.10.0" +aws-sdk-dynamodb = "1.32.0" +futures = "0.3.1" # wasm dependencies serde-wasm-bindgen = { version = "0.6", optional = true } diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs new file mode 100644 index 0000000000..e825290656 --- /dev/null +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -0,0 +1,1882 @@ +//! Entity Slicing + +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::fmt::{Display, Formatter}; +use std::hash::RandomState; +use std::sync::Arc; + +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::{ + BinaryOp, Entity, EntityUID, Expr, ExprKind, Literal, PartialValue, PolicySet, Request, + RequestType, UnaryOp, Value, ValueKind, Var, + }, + entities::Entities, +}; +use miette::Diagnostic; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use smol_str::SmolStr; +use thiserror::Error; + +use crate::ValidationError; +use crate::{ + typecheck::{PolicyCheck, Typechecker}, + types::{EntityRecordKind, Type}, + ValidationMode, ValidatorSchema, +}; + +type PerAction = HashMap>; +type FlatPerAction = HashMap; + +/// Data structure that tells the user what data is needed +/// based on the action's ID +#[serde_as] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EntityManifest +where + T: Clone, +{ + /// A map from actions to primary slice + #[serde_as(as = "Vec<(_, _)>")] + #[serde(bound(deserialize = "T: Default"))] + pub per_action: PerAction, +} + +/// A flattened version of an [`EntityManifest`] +#[derive(Debug)] +pub struct FlatEntityManifest { + /// For each action, all the data paths required + pub per_action: FlatPerAction, +} + +/// A map of data fields to entity slices +pub type Fields = HashMap>>; + +/// The root of an entity slice. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub enum EntityRoot { + /// Literal entity ids + Literal(EntityUID), + /// A Cedar variable + 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}"), + } + } +} + +/// a [`PrimarySlice`] is a tree that tells you what data to load +#[serde_as] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PrimarySlice +where + T: Clone, +{ + #[serde_as(as = "Vec<(_, _)>")] + #[serde(bound(deserialize = "T: Default"))] + /// The data that needs to be loaded, organized by root + pub trie: HashMap>, +} + +/// A flattened version of a [`PrimarySlice`] +#[derive(Debug)] +pub struct FlatPrimarySlice { + /// All the paths of data required, each starting with a root [`Var`] + pub data: Vec, +} + +/// An entity slice- tells users a tree of data to load +#[serde_as] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EntitySlice { + /// Child data of this entity slice. + #[serde_as(as = "Vec<(_, _)>")] + pub children: Fields, + /// For entity types, this boolean may be `true` + /// to signal that all the parents in the entity hierarchy + /// are required (transitively). + pub parents_required: bool, + /// Optional data annotation, usually used for type information. + #[serde(skip_serializing, skip_deserializing)] + #[serde(bound(deserialize = "T: Default"))] + pub data: T, +} + +/// A data path that may end with requesting the parents of +/// an entity. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FlatEntitySlice { + /// The root variable that begins the data path + pub root: EntityRoot, + /// The path of fields of entities or structs + pub path: Vec, + /// Request all the parents in the entity hierarchy of this entity. + pub parents_required: bool, +} + +/// An error generated by entity slicing. +#[derive(Debug, Error, Diagnostic)] +#[non_exhaustive] +pub enum EntitySliceError { + /// A validation error was encountered + #[error(transparent)] + #[diagnostic(transparent)] + ValidationError(#[from] ValidationError), + /// A entities error was encountered + #[error(transparent)] + #[diagnostic(transparent)] + EntitiesError(#[from] EntitiesError), + + /// The request was partial + #[error("Entity slicing requires a fully concrete request. Got a partial request.")] + PartialRequestError, + /// A policy was partial + #[error( + "Entity slicing requires fully concrete policies. Got a policy with an unknown expression." + )] + PartialExpressionError, + + /// A policy was not analyzable because it used operators + /// before a [`ExprKind::GetAttr`] + /// TODO make a more specific error that includes the expression + #[error("Failed to analyze policy: mixed getting attributes with other operators")] + FailedAnalysis, + + /// During entity loading, failed to find an entity. + #[error("Missing entity `{0}` during entity loading.")] + MissingEntity(EntityUID), + + /// During entity loading, attempted to load from + /// a type without fields. + #[error("Expected entity or record during entity loading. Got value: {0}")] + IncompatibleEntityManifest(Value), + + /// Found a partial entity during entity loading. + #[error("Found partial entity while doing entity slicing.")] + PartialEntity, + + /// During entity loading using the simplified API, + /// the entity loader returned the wrong number of entities. + #[error("Wrong number of entities returned ({0}). Expected {1}.")] + WrongNumberOfEntities(usize, usize), +} + +fn union_fields(first: &Fields, second: &Fields) -> Fields { + let mut res = first.clone(); + for (key, value) in second { + if let Some(existing) = res.get(key) { + res.insert(key.clone(), Box::new((*existing).union(value))); + } else { + res.insert(key.clone(), value.clone()); + } + } + res +} + +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_concrete_env() + .ok_or(EntitySliceError::PartialRequestError)?; + self.per_action + .get(&request_type) + .map(|primary| primary.slice_entities(entities, request)) + .unwrap_or(Ok(Entities::default())) + } + + /// Flatten this manifest into a [`FlatEntityManifest`] + pub fn to_flat_entity_manifest(&self) -> FlatEntityManifest { + let mut per_action: FlatPerAction = Default::default(); + + for (action, primary) in &self.per_action { + per_action.insert(action.clone(), primary.to_flat_primary_slice()); + } + FlatEntityManifest { per_action } + } + + /// Convert this manifest into a human-readable format. + /// The format specifies the request types, then + /// prints all the flattened paths as cedar expressions. + pub fn to_str_natural(&self) -> String { + let flattened = self.to_flat_entity_manifest(); + let exprs = flattened.to_exprs(); + let mut res = String::new(); + for (types, exprs) in exprs { + res.push_str(&format!("{} {{\n", types.to_str_natural())); + for expr in exprs { + res.push_str(&format!(" {}\n", expr)); + } + res.push_str("}\n"); + } + res + } +} + +impl FlatEntityManifest { + /// Convert this flattened manifest into a list of cedar expressions. + /// The expressions only use the constructors [`ExprKind::GetAttr`] and [`Var`]. + /// They may also have the shape `principal in expr` to denote that + /// the parents of `expr` are needed. + pub fn to_exprs(&self) -> HashMap> { + let mut res: HashMap> = Default::default(); + for (types, path) in &self.per_action { + let exprs = path.to_exprs(); + res.insert(types.clone(), exprs); + } + res + } +} + +impl FlatPrimarySlice { + fn to_exprs(&self) -> Vec { + let mut res = vec![]; + for slice in &self.data { + let expr = slice.to_expr(); + res.push(expr); + } + res + } +} + +impl FlatEntitySlice { + /// Given a path of fields to access, convert to a tree + /// (the [`Fields`] data structure. + /// Also, when we need to pull all the data for the final field + /// do so. + fn to_primary_slice(&self) -> PrimarySlice { + self.to_primary_slice_with_leaf(EntitySlice { + parents_required: true, + children: Default::default(), + data: (), + }) + } + + fn to_primary_slice_with_leaf(&self, leaf_entity: EntitySlice) -> PrimarySlice { + let mut current = leaf_entity; + // reverse the path, visiting the last access first + for field in self.path.iter().rev() { + let mut fields = HashMap::new(); + fields.insert(field.clone(), Box::new(current)); + current = EntitySlice { + parents_required: false, + children: fields, + data: (), + }; + } + + let mut primary_map = HashMap::new(); + primary_map.insert(self.root.clone(), current); + PrimarySlice { trie: primary_map } + } + + fn to_expr(&self) -> Expr { + let mut expr = self.root.to_expr(); + + for field in &self.path { + expr = Expr::get_attr(expr, field.clone()); + } + + if self.parents_required { + expr = Expr::binary_app(BinaryOp::In, Expr::var(Var::Principal), expr); + } + expr + } + + /// Converts compatible expressions to a [`FlatEntitySlice`] + /// Compatible expressions start with a variable, with + /// any number of [`ExprKin::GetAttr`], and optionally are wrapped with + /// `principal in ` + fn from_expr(expr: &Expr) -> Option { + let (mut current_expr, parents_required) = match expr.expr_kind() { + ExprKind::BinaryApp { + op: BinaryOp::In, + arg1, + arg2, + } => { + if **arg1 != Expr::var(Var::Principal) { + return None; + } + (arg2.clone(), true) + } + _ => (Arc::new(expr.clone()), false), + }; + + let mut path = vec![]; + loop { + match current_expr.expr_kind() { + ExprKind::GetAttr { expr, attr } => { + path.push(attr.clone()); + current_expr = expr.clone(); + } + ExprKind::Var(var) => { + path.reverse(); + + return Some(FlatEntitySlice { + root: EntityRoot::Var(*var), + path, + parents_required, + }); + } + ExprKind::Lit(Literal::EntityUID(literal)) => { + path.reverse(); + + return Some(FlatEntitySlice { + root: EntityRoot::Literal((**literal).clone()), + path, + parents_required, + }); + } + _ => return None, + } + } + } +} + +impl EntityRoot { + /// Convert this root to a cedar expression. + /// This will either be a variable or a literal. + pub fn to_expr(&self) -> Expr { + match self { + Self::Literal(lit) => Expr::val(Literal::EntityUID(Arc::new(lit.clone()))), + Self::Var(var) => Expr::var(*var), + } + } +} + +impl PrimarySlice { + /// 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(EntitySliceError::PartialRequestError)?; + slice.slice_entity(entities, entity_id, &mut res)?; + } + EntityRoot::Var(Var::Principal) => { + let entity_id = request + .principal() + .uid() + .ok_or(EntitySliceError::PartialRequestError)?; + slice.slice_entity(entities, entity_id, &mut res)?; + } + EntityRoot::Var(Var::Resource) => { + let resource_id = request + .resource() + .uid() + .ok_or(EntitySliceError::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(EntitySliceError::PartialRequestError)? + .clone(), + ); + let PartialValue::Value(val) = partial_val else { + return Err(EntitySliceError::PartialRequestError); + }; + slice.slice_val(entities, &val, &mut res); + } + } + } + } + Ok(Entities::from_entities( + res.into_values(), + None::<&NoEntitiesSchema>, + TCComputation::AssumeAlreadyComputed, + Extensions::all_available(), + )?) + } + + fn to_flat_primary_slice(&self) -> FlatPrimarySlice { + let mut data: Vec = vec![]; + + for (root, slice) in &self.trie { + for flattened in slice.to_flat_entity_slice(root) { + data.push(flattened); + } + } + + FlatPrimarySlice { data } + } + + /// Create an empty [`PrimarySlice`] that requires no data + pub fn new() -> Self { + Self { + trie: Default::default(), + } + } +} + +impl PrimarySlice { + /// Union two [`PrimarySlice`]s together, requiring + /// the data that both of them require + fn union(&self, other: &Self) -> Self { + let mut res = self.clone(); + for (key, value) in &other.trie { + if let Some(existing) = res.trie.get(key) { + res.trie.insert(key.clone(), (*existing).union(value)); + } else { + res.trie.insert(key.clone(), value.clone()); + } + } + res + } +} + +impl Default for PrimarySlice { + fn default() -> Self { + Self::new() + } +} + +impl EntitySlice { + /// Union two [`EntitySlice`]s together, requiring + /// the data that both of them require + fn union(&self, other: &Self) -> Self { + Self { + children: union_fields(&self.children, &other.children), + parents_required: self.parents_required || other.parents_required, + data: self.data.clone(), + } + } +} + +impl EntitySlice { + /// 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(EntitySliceError::PartialEntity); + }; + let sliced = slice.slice_val(entities, &val, res)?; + + new_entity.insert(field.clone(), PartialValue::Value(sliced)); + } + } + + let new_ancestors = if self.parents_required { + entity.ancestors().cloned().collect() + } else { + HashSet::new() + }; + + let new_entity = + Entity::new_with_attr_partial_value(lit.clone(), new_entity, new_ancestors); + + #[allow(clippy::expect_used)] + if let Some(existing) = res.get_mut(lit) { + // Here we union the new entity with any existing one + // PANIC SAFETY: Entities in the entity store with the same ID should be compatible to union together. + *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.parents_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(EntitySliceError::IncompatibleEntityManifest(val.clone())); + } + + 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) + } + }) + } + + fn new() -> Self { + Self { + children: Default::default(), + parents_required: false, + data: (), + } + } + + fn to_flat_entity_slice(&self, root: &EntityRoot) -> Vec { + let mut flattened_reversed = self.flatten_reversed(root); + for flattened in flattened_reversed.iter_mut() { + flattened.path.reverse(); + } + flattened_reversed + } + + /// Builds a [`FlatEntitySlice`] in reversed order for efficient + /// `push` operation. + fn flatten_reversed(&self, root: &EntityRoot) -> Vec { + if self.children.is_empty() { + vec![FlatEntitySlice { + root: root.clone(), + path: vec![], + parents_required: false, + }] + } else { + let mut res = vec![]; + for (key, value) in &self.children { + for mut flattened in value.flatten_reversed(root) { + if flattened.parents_required { + res.push(flattened.clone()); + } + flattened.path.push(key.clone()); + flattened.parents_required = self.parents_required; + res.push(flattened); + } + } + res + } + } +} + +/// Computes an [`EntitySliceManifest`] from the schema and policies +pub fn compute_entity_slice_manifest( + schema: &ValidatorSchema, + policies: &PolicySet, +) -> Result { + let mut manifest: HashMap = HashMap::new(); + + // now, for each policy we add the data it requires to the manifest + for policy in policies.policies() { + // typecheck the policy and get all the request environments + let typechecker = Typechecker::new(schema, ValidationMode::Strict, policy.id().clone()); + let request_envs = typechecker.typecheck_by_request_env(policy.template()); + for (request_env, policy_check) in request_envs { + // match on the typechecking answer + let new_primary_slice = match policy_check { + PolicyCheck::Success(typechecked_expr) => compute_primary_slice(&typechecked_expr), + PolicyCheck::Irrelevant(_) => { + // always results in false, + // so we need no data + + Ok(PrimarySlice::new()) + } + // TODO is returning the first error correct? + // Also, should we run full validation instead of just + // typechecking? + PolicyCheck::Fail(errors) => { + // PANIC SAFETY policy check fail + // should be a non-empty vector. + #[allow(clippy::expect_used)] + Err(errors + .first() + .expect("Policy check failed without an error") + .clone() + .into()) + } + }?; + + let request_types = request_env + .to_request_types() + .ok_or(EntitySliceError::PartialRequestError)?; + if let Some(existing) = manifest.get_mut(&request_types) { + *existing = existing.union(&new_primary_slice); + } else { + manifest.insert(request_types, new_primary_slice); + } + } + } + + Ok(EntityManifest { + per_action: manifest, + }) +} + +fn compute_primary_slice(expr: &Expr>) -> Result { + let mut primary_slice = PrimarySlice::new(); + add_to_primary_slice(&mut primary_slice, expr, false)?; + Ok(primary_slice) +} + +fn add_to_primary_slice( + primary_slice: &mut PrimarySlice, + expr: &Expr>, + should_load_all: bool, +) -> Result<(), EntitySliceError> { + match expr.expr_kind() { + // Literals, variables, and unkonwns without any GetAttr operations + // on them are okay, since no fields need to be loaded. + ExprKind::Lit(_) => (), + ExprKind::Var(_) => (), + ExprKind::Slot(_) => (), + ExprKind::Unknown(_) => return Err(EntitySliceError::PartialExpressionError), + ExprKind::If { + test_expr, + then_expr, + else_expr, + } => { + add_to_primary_slice(primary_slice, test_expr, should_load_all)?; + add_to_primary_slice(primary_slice, then_expr, should_load_all)?; + add_to_primary_slice(primary_slice, else_expr, should_load_all)?; + } + ExprKind::And { left, right } => { + add_to_primary_slice(primary_slice, left, should_load_all)?; + add_to_primary_slice(primary_slice, right, should_load_all)?; + } + ExprKind::Or { left, right } => { + add_to_primary_slice(primary_slice, left, should_load_all)?; + add_to_primary_slice(primary_slice, right, should_load_all)?; + } + // For unary and binary operations, we need to be careful + // to remain sound. + // For example, equality requires that we pull all data + ExprKind::UnaryApp { op, arg } => match op { + UnaryOp::Not => add_to_primary_slice(primary_slice, arg, should_load_all)?, + UnaryOp::Neg => add_to_primary_slice(primary_slice, arg, should_load_all)?, + }, + ExprKind::BinaryApp { op, arg1, arg2 } => match op { + BinaryOp::Eq => { + add_to_primary_slice(primary_slice, arg1, true)?; + add_to_primary_slice(primary_slice, arg1, true)?; + } + BinaryOp::In => { + // add arg2 to primary slice + add_to_primary_slice(primary_slice, arg2, should_load_all)?; + + // get the path for arg1 + let mut flat_slice = get_expr_path(arg1)?; + flat_slice.parents_required = true; + *primary_slice = primary_slice.union(&flat_slice.to_primary_slice()); + } + BinaryOp::Contains | BinaryOp::ContainsAll | BinaryOp::ContainsAny => { + add_to_primary_slice(primary_slice, arg1, true)?; + add_to_primary_slice(primary_slice, arg2, true)?; + } + BinaryOp::Less | BinaryOp::LessEq | BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul => { + add_to_primary_slice(primary_slice, arg1, should_load_all)?; + add_to_primary_slice(primary_slice, arg2, should_load_all)?; + } + }, + ExprKind::ExtensionFunctionApp { fn_name: _, args } => { + // WARNING: this code assumes that extension functions + // don't take full structs as inputs. + // If they did, we would need to use logic similar to the Eq binary operator. + for arg in args.iter() { + add_to_primary_slice(primary_slice, arg, should_load_all)?; + } + } + ExprKind::Like { expr, pattern: _ } => { + add_to_primary_slice(primary_slice, expr, should_load_all)?; + } + ExprKind::Is { + expr, + entity_type: _, + } => { + add_to_primary_slice(primary_slice, expr, should_load_all)?; + } + ExprKind::Set(contents) => { + for expr in &**contents { + add_to_primary_slice(primary_slice, expr, should_load_all)?; + } + } + ExprKind::Record(content) => { + for expr in content.values() { + add_to_primary_slice(primary_slice, expr, should_load_all)?; + } + } + ExprKind::HasAttr { expr, attr } => { + let mut flat_slice = get_expr_path(expr)?; + flat_slice.path.push(attr.clone()); + *primary_slice = primary_slice.union(&flat_slice.to_primary_slice()); + } + ExprKind::GetAttr { .. } => { + let flat_slice = get_expr_path(expr)?; + + #[allow(clippy::expect_used)] + let leaf_field = if should_load_all { + entity_slice_from_type( + expr.data() + .as_ref() + .expect("Typechecked expression missing type"), + ) + } else { + EntitySlice::new() + }; + + *primary_slice = flat_slice.to_primary_slice_with_leaf(leaf_field); + } + }; + + Ok(()) +} + +fn full_tree_for_entity_or_record(ty: &EntityRecordKind) -> Fields<()> { + match ty { + EntityRecordKind::ActionEntity { name: _, attrs } + | EntityRecordKind::Record { + attrs, + open_attributes: _, + } => { + let mut fields = HashMap::new(); + for (attr_name, attr_type) in attrs.iter() { + fields.insert( + attr_name.clone(), + Box::new(entity_slice_from_type(&attr_type.attr_type)), + ); + } + fields + } + + EntityRecordKind::Entity(_) | EntityRecordKind::AnyEntity => { + // no need to load data for entities, which are compared + // using ids + Default::default() + } + } +} + +fn entity_slice_from_type(ty: &Type) -> EntitySlice { + match ty { + // if it's not an entity or record, slice ends here + Type::ExtensionType { .. } + | Type::Never + | Type::True + | Type::False + | Type::Primitive { .. } + | Type::Set { .. } => EntitySlice::new(), + Type::EntityOrRecord(record_type) => EntitySlice { + children: full_tree_for_entity_or_record(record_type), + parents_required: false, + data: (), + }, + } +} + +/// Given an expression, get the corresponding data path +/// starting with a variable. +fn get_expr_path(expr: &Expr>) -> Result { + Ok(match expr.expr_kind() { + ExprKind::Slot(slot_id) => { + if slot_id.is_principal() { + FlatEntitySlice { + root: EntityRoot::Var(Var::Principal), + path: vec![], + parents_required: false, + } + } else { + assert!(slot_id.is_resource()); + FlatEntitySlice { + root: EntityRoot::Var(Var::Resource), + path: vec![], + parents_required: false, + } + } + } + ExprKind::Var(var) => FlatEntitySlice { + root: EntityRoot::Var(*var), + path: vec![], + parents_required: false, + }, + ExprKind::GetAttr { expr, attr } => { + let mut slice = get_expr_path(expr)?; + slice.path.push(attr.clone()); + slice + } + ExprKind::Lit(Literal::EntityUID(literal)) => FlatEntitySlice { + root: EntityRoot::Literal((**literal).clone()), + path: vec![], + parents_required: false, + }, + _ => Err(EntitySliceError::FailedAnalysis)?, + }) +} + +/// Loads all the entities needed for a request given a function +/// that loads an entire entity by id. +/// Assumes that the entire context will be loaded separately. +/// TODO make not public +pub fn load_entities_simplified( + //schema: &ValidatorSchema, + manifest: &EntityManifest, + request: &Request, + loader: &mut impl FnMut(&[&EntityUID]) -> Vec, +) -> Result { + let Some(primary_slice) = manifest.per_action.get( + &request + .to_concrete_env() + .ok_or(EntitySliceError::PartialRequestError)?, + ) else { + // if the request type isn't in the manifest, we need no data + return Entities::from_entities( + vec![], + None::<&NoEntitiesSchema>, + TCComputation::AssumeAlreadyComputed, + Extensions::all_available(), + ) + .map_err(|err| err.into()); + }; + + let mut entities: HashMap = Default::default(); + + for (key, value) in &primary_slice.trie { + match key { + EntityRoot::Var(Var::Principal) => { + load_entity_slice( + loader, + &mut entities, + request + .principal() + .uid() + .ok_or(EntitySliceError::PartialRequestError)?, + value, + )?; + } + EntityRoot::Var(Var::Action) => { + load_entity_slice( + loader, + &mut entities, + request + .action() + .uid() + .ok_or(EntitySliceError::PartialRequestError)?, + value, + )?; + } + EntityRoot::Var(Var::Resource) => { + load_entity_slice( + loader, + &mut entities, + request + .resource() + .uid() + .ok_or(EntitySliceError::PartialRequestError)?, + value, + )?; + } + EntityRoot::Literal(lit) => { + load_entity_slice(loader, &mut entities, lit, value)?; + } + EntityRoot::Var(Var::Context) => { + // skip context, since the simplified loader assumes the entire context is loaded + } + } + } + + Entities::from_entities( + entities.values().cloned(), + None::<&NoEntitiesSchema>, + TCComputation::AssumeAlreadyComputed, + Extensions::all_available(), + ) + .map_err(|err| err.into()) +} + +fn load_entity_slice( + loader: &mut impl FnMut(&[&EntityUID]) -> Vec, + entities: &mut HashMap, + entity: &EntityUID, + slice: &EntitySlice, +) -> Result<(), EntitySliceError> { + // special case: no need to load anything for empty fields with no parents required + if slice.children.is_empty() && !slice.parents_required { + return Ok(()); + } + + let new_entities = loader(&[entity]); + if new_entities.len() != 1 { + return Err(EntitySliceError::WrongNumberOfEntities( + 1, + new_entities.len(), + )); + } + #[allow(clippy::expect_used)] + let new_entity = new_entities + .into_iter() + .next() + .expect("Vector has length 1 as shown by if statement above."); + + // now we need to load any entity references + let remaining_entities = find_remaining_entities(&new_entity, &slice.children); + + for (id, slice) in remaining_entities? { + load_entity_slice(loader, entities, &id, &slice)?; + } + + // TODO also need to load parents of some entities + + entities.insert(new_entity.uid().clone(), new_entity); + Ok(()) +} + +/// This helper function finds all entity references that need to be +/// loaded given an already-loaded [`Entity`] and corresponding [`Fields`]. +/// Returns pairs of entity and slices that need to be loaded. +pub fn find_remaining_entities( + entity: &Entity, + fields: &Fields<()>, +) -> Result, EntitySliceError> { + let mut remaining = HashMap::new(); + for (field, slice) in fields { + if let Some(pvalue) = entity.get(field) { + let PartialValue::Value(value) = pvalue else { + return Err(EntitySliceError::PartialEntity); + }; + find_remaining_entities_value(&mut remaining, value, slice)?; + } + } + + Ok(remaining) +} + +fn find_remaining_entities_value( + remaining: &mut HashMap, + value: &Value, + slice: &EntitySlice, +) -> Result<(), EntitySliceError> { + match value.value_kind() { + ValueKind::Lit(literal) => match literal { + Literal::EntityUID(entity_id) => { + if let Some(existing) = remaining.get_mut(entity_id) { + *existing = existing.union(slice); + } else { + remaining.insert((**entity_id).clone(), slice.clone()); + } + } + _ => assert!(slice.children.is_empty()), + }, + ValueKind::Set(_) => { + assert!(slice.children.is_empty()); + } + ValueKind::ExtensionValue(_) => { + assert!(slice.children.is_empty()); + } + ValueKind::Record(record) => { + for (field, child_slice) in &slice.children { + // only need to slice if field is present + if let Some(value) = record.get(field) { + find_remaining_entities_value(remaining, value, child_slice)?; + } + } + } + }; + Ok(()) +} + +#[cfg(test)] +mod entity_slice_tests { + use cedar_policy_core::{ast::{Context, PolicyID}, entities::EntityJsonParser, parser::parse_policy}; + + use crate::CoreSchema; + + use super::*; + + 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(); + + #[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 = ValidatorSchema::from_str_natural( + " +entity User = { + name: String, +}; + +entity Document; + +action Read appliesTo { + principal: [User], + resource: [Document] +}; + ", + Extensions::all_available(), + ) + .unwrap() + .0; + + let entity_manifest = + compute_entity_slice_manifest(&schema, &pset).expect("Should succeed"); + let expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "name", + { + "children": [], + "parents_required": false + } + ] + ], + "parents_required": false + } + ] + ] + } + ] + ] +}"#; + let expected_manifest = serde_json::from_str(expected).unwrap(); + assert_eq!(entity_manifest, expected_manifest); + + 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, + &expected_manifest, + ); + } + + #[test] + #[should_panic] + 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 = ValidatorSchema::from_str_natural( + " +entity User = { + name: String, +}; + +entity Document; + +action Read appliesTo { + principal: [User], + resource: [Document] +}; + ", + Extensions::all_available(), + ) + .unwrap() + .0; + + let entity_manifest = + compute_entity_slice_manifest(&schema, &pset).expect("Should succeed"); + let expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + ] + } + ] + ] +}"#; + let expected_manifest = serde_json::from_str(expected).unwrap(); + assert_eq!(entity_manifest, expected_manifest); + + 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, + &expected_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 = ValidatorSchema::from_str_natural( + " +entity User = { + name: String, +}; + +entity Document; + +action Read appliesTo { + principal: [User], + resource: [Document] +}; + ", + Extensions::all_available(), + ) + .unwrap() + .0; + + let entity_manifest = + compute_entity_slice_manifest(&schema, &pset).expect("Should succeed"); + let expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + ] + } + ] + ] +}"#; + let expected_manifest = serde_json::from_str(expected).unwrap(); + assert_eq!(entity_manifest, expected_manifest); + + 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, + &expected_manifest, + ); + } + + #[test] + fn test_entity_manifest_parents_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_str_natural( + " +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_slice_manifest(&schema, &pset).expect("Should succeed"); + let expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "manager", + { + "children": [], + "parents_required": true + } + ] + ], + "parents_required": true + } + ] + ] + } + ] + ] +}"#; + let expected_manifest = serde_json::from_str(expected).unwrap(); + assert_eq!(entity_manifest, expected_manifest); + + 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, + &expected_manifest, + ); + } + + #[test] + fn test_entity_manifest_multiple_types() { + 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 = ValidatorSchema::from_str_natural( + " +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 entity_manifest = + compute_entity_slice_manifest(&schema, &pset).expect("Should succeed"); + let expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "name", + { + "children": [], + "parents_required": false + } + ] + ], + "parents_required": false + } + ] + ] + } + ], + [ + { + "principal": "OtherUserType", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "name", + { + "children": [], + "parents_required": false + } + ] + ], + "parents_required": false + } + ] + ] + } + ] + ] +}"#; + let expected_manifest = serde_json::from_str(expected).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_str_natural( + " +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_slice_manifest(&schema, &pset).expect("Should succeed"); + let expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "resource" + }, + { + "children": [ + [ + "metadata", + { + "children": [ + [ + "owner", + { + "children": [], + "parents_required": false + } + ] + ], + "parents_required": false + } + ], + [ + "readers", + { + "children": [], + "parents_required": false + } + ] + ], + "parents_required": false + } + ] + ] + } + ] + ] +}"#; + let expected_manifest = serde_json::from_str(expected).unwrap(); + assert_eq!(entity_manifest, expected_manifest); + + 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, + &expected_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_str_natural( + " +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_slice_manifest(&schema, &pset).expect("Should succeed"); + let expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "BeSad" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "metadata", + { + "children": [ + [ + "nickname", + { + "children": [], + "parents_required": false + } + ], + [ + "friends", + { + "children": [], + "parents_required": false + } + ] + ], + "parents_required": false + } + ] + ], + "parents_required": false + } + ] + ] + } + ] + ] +}"#; + let expected_manifest = serde_json::from_str(expected).unwrap(); + assert_eq!(entity_manifest, expected_manifest); + } +} diff --git a/cedar-policy-validator/src/err.rs b/cedar-policy-validator/src/err.rs index e5a798291b..f6e74463e1 100644 --- a/cedar-policy-validator/src/err.rs +++ b/cedar-policy-validator/src/err.rs @@ -22,8 +22,12 @@ use itertools::{Either, Itertools}; use miette::Diagnostic; use nonempty::NonEmpty; use thiserror::Error; +<<<<<<< HEAD use crate::cedar_schema; +======= +use crate::human_schema; +>>>>>>> b9d23316 (Entity slicing implementation) /// Error creating a schema from the Cedar syntax #[derive(Debug, Error, Diagnostic)] diff --git a/cedar-policy-validator/src/lib.rs b/cedar-policy-validator/src/lib.rs index ef176b57b9..9a8995f89c 100644 --- a/cedar-policy-validator/src/lib.rs +++ b/cedar-policy-validator/src/lib.rs @@ -35,6 +35,7 @@ use cedar_policy_core::ast::{Policy, PolicySet, Template}; use serde::Serialize; use std::collections::HashSet; +pub mod entity_slicing; mod err; pub use err::*; mod coreschema; diff --git a/cedar-policy-validator/src/typecheck.rs b/cedar-policy-validator/src/typecheck.rs index e720f8e4ca..f055d81146 100644 --- a/cedar-policy-validator/src/typecheck.rs +++ b/cedar-policy-validator/src/typecheck.rs @@ -40,7 +40,7 @@ use crate::{ use cedar_policy_core::ast::{ BinaryOp, EntityType, EntityUID, Expr, ExprBuilder, ExprKind, Literal, Name, PolicyID, - PrincipalOrResourceConstraint, SlotId, Template, UnaryOp, Var, + PrincipalOrResourceConstraint, SlotId, Template, UnaryOp, Var, ACTION_ENTITY_TYPE, }; #[cfg(not(target_arch = "wasm32"))] @@ -2306,3 +2306,33 @@ impl<'a> Typechecker<'a> { } } } + +impl Typechecker<'_> { + /// Typecheck an expression outside the context of a policy. This is + /// currently only used for testing. + pub(crate) fn typecheck_expr<'a>( + &self, + e: &'a Expr, + unique_type_errors: &mut HashSet, + ) -> TypecheckAnswer<'a> { + // Using bogus entity type names here for testing. They'll be treated as + // having empty attribute records, so tests will behave as expected. + let request_env = RequestEnv::DeclaredAction { + principal: &"Principal" + .parse() + .expect("Placeholder type \"Principal\" failed to parse as valid type name."), + action: &EntityUID::with_eid_and_type(ACTION_ENTITY_TYPE, "action") + .expect("ACTION_ENTITY_TYPE failed to parse as type name."), + resource: &"Resource" + .parse() + .expect("Placeholder type \"Resource\" failed to parse as valid type name."), + context: &Type::record_with_attributes(None, OpenTag::ClosedAttributes), + principal_slot: None, + resource_slot: None, + }; + let mut type_errors = Vec::new(); + let ans = self.typecheck(&request_env, &EffectSet::new(), e, &mut type_errors); + unique_type_errors.extend(type_errors); + ans + } +} diff --git a/cedar-policy-validator/src/typecheck/test/test_utils.rs b/cedar-policy-validator/src/typecheck/test/test_utils.rs index 603466cc16..56cb14b704 100644 --- a/cedar-policy-validator/src/typecheck/test/test_utils.rs +++ b/cedar-policy-validator/src/typecheck/test/test_utils.rs @@ -78,36 +78,6 @@ impl Type { } } -impl Typechecker<'_> { - /// Typecheck an expression outside the context of a policy. This is - /// currently only used for testing. - pub(crate) fn typecheck_expr<'a>( - &self, - e: &'a Expr, - unique_type_errors: &mut HashSet, - ) -> TypecheckAnswer<'a> { - // Using bogus entity type names here for testing. They'll be treated as - // having empty attribute records, so tests will behave as expected. - let request_env = RequestEnv::DeclaredAction { - principal: &"Principal" - .parse() - .expect("Placeholder type \"Principal\" failed to parse as valid type name."), - action: &EntityUID::with_eid_and_type(ACTION_ENTITY_TYPE, "action") - .expect("ACTION_ENTITY_TYPE failed to parse as type name."), - resource: &"Resource" - .parse() - .expect("Placeholder type \"Resource\" failed to parse as valid type name."), - context: &Type::record_with_attributes(None, OpenTag::ClosedAttributes), - principal_slot: None, - resource_slot: None, - }; - let mut type_errors = Vec::new(); - let ans = self.typecheck(&request_env, &CapabilitySet::new(), e, &mut type_errors); - unique_type_errors.extend(type_errors); - ans - } -} - /// Assert expected == actual by by asserting expected <: actual && actual <: expected. /// In the future it might better to only assert actual <: expected to allow /// improvement to the typechecker to return more specific types. diff --git a/cedar-policy-validator/src/types.rs b/cedar-policy-validator/src/types.rs index b9d5357906..b202a1f35e 100644 --- a/cedar-policy-validator/src/types.rs +++ b/cedar-policy-validator/src/types.rs @@ -23,7 +23,7 @@ mod request_env; pub use request_env::*; use itertools::Itertools; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use smol_str::SmolStr; use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, @@ -46,7 +46,7 @@ use crate::{validation_errors::LubHelp, ValidationMode}; use super::schema::{ValidatorActionId, ValidatorEntityType, ValidatorSchema}; /// The main type structure. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] pub enum Type { /// Bottom type. Sub-type of all types. Never, @@ -763,7 +763,7 @@ impl TryFrom for CoreSchemaType { /// Represents the least upper bound of multiple entity types. This can be used /// to represent the least upper bound of a single entity type, in which case it /// is exactly that entity type. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] pub struct EntityLUB { /// We store `EntityType` here because these are entity types. /// As of this writing, `EntityType` is backed by `Name` (rather than @@ -900,7 +900,7 @@ impl EntityLUB { /// Represents the attributes of a record or entity type. Each attribute has an /// identifier, a flag indicating weather it is required, and a type. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Default)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Default, Deserialize)] pub struct Attributes { /// Attributes map pub attrs: BTreeMap, @@ -1052,7 +1052,7 @@ impl IntoIterator for Attributes { /// Used to tag record types to indicate if their attributes record is open or /// closed. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Copy, Clone, Serialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Copy, Clone, Serialize, Deserialize)] pub enum OpenTag { /// The attributes are open. A value of this type may have attributes other /// than those listed. @@ -1075,7 +1075,7 @@ impl OpenTag { /// /// The subtyping lattice for these types is that /// `Entity` <: `AnyEntity`. `Record` does not subtype anything. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] pub enum EntityRecordKind { /// A record type, with these attributes Record { @@ -1355,7 +1355,7 @@ impl EntityRecordKind { } /// Contains the type of a record attribute and if the attribute is required. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AttributeType { /// The type of the attribute. @@ -1384,7 +1384,7 @@ impl AttributeType { } /// Represent the possible primitive types. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] pub enum Primitive { /// Primitive boolean type. Bool, diff --git a/cedar-policy-validator/src/types/request_env.rs b/cedar-policy-validator/src/types/request_env.rs index 16f27aa30f..a9879ded1a 100644 --- a/cedar-policy-validator/src/types/request_env.rs +++ b/cedar-policy-validator/src/types/request_env.rs @@ -14,7 +14,8 @@ * limitations under the License. */ -use cedar_policy_core::ast::{EntityType, EntityUID}; +use cedar_policy_core::ast::{EntityType, EntityUID, RequestType}; +use serde::Serialize; use crate::ValidatorSchema; @@ -22,7 +23,7 @@ use super::Type; /// Represents a request type environment. In principle, this contains full /// types for the four variables (principal, action, resource, context). -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)] pub enum RequestEnv<'a> { /// Contains the four variables bound in the type environment. These together /// represent the full type of (principal, action, resource, context) @@ -49,6 +50,26 @@ pub enum RequestEnv<'a> { } impl<'a> RequestEnv<'a> { + /// Return the types of each of the elements of this request. + /// Returns [`None`] when the request is not fully concrete. + pub fn to_request_types(&self) -> Option { + match self { + RequestEnv::DeclaredAction { + principal, + action, + resource, + context: _, + principal_slot: _, + resource_slot: _, + } => Some(RequestType { + principal: (*principal).clone(), + action: (*action).clone(), + resource: (*resource).clone(), + }), + RequestEnv::UndeclaredAction => None, + } + } + /// The principal type for this request environment, as an [`EntityType`]. /// `None` indicates we don't know (only possible in partial schema validation). pub fn principal_entity_type(&self) -> Option<&'a EntityType> { diff --git a/cedar-policy/Cargo.toml b/cedar-policy/Cargo.toml index 94a182c777..d5148edda6 100644 --- a/cedar-policy/Cargo.toml +++ b/cedar-policy/Cargo.toml @@ -26,6 +26,7 @@ smol_str = { version = "0.2", features = ["serde"] } dhat = { version = "0.3.2", optional = true } serde_with = "3.3.0" nonempty = "0.10" +aws-sdk-dynamodb = "1.32.0" # wasm dependencies serde-wasm-bindgen = { version = "0.6", optional = true } diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index cd1e453157..e94a031b52 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -24,6 +24,9 @@ mod id; use cedar_policy_validator::typecheck::{PolicyCheck, Typechecker}; +pub use cedar_policy_validator::entity_slicing::{ + self, compute_entity_slice_manifest, EntityManifest, EntitySliceError, +}; pub use id::*; mod err; @@ -31,9 +34,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, EntityUID}; use cedar_policy_core::authorizer; use cedar_policy_core::entities::{ContextSchema, Dereference}; use cedar_policy_core::est::{self, TemplateLink}; @@ -4271,3 +4274,51 @@ action CreateList in Create appliesTo { assert_eq!(entities, expected); } } + + +/// Given a schema and policy set, compute an entity slice manifest. +/// The manifest describes the data required to answer requests +/// for each action type. +pub fn compute_entity_manifest( + schema: &Schema, + pset: &PolicySet, +) -> Result { + compute_entity_slice_manifest(&schema.0, &pset.ast) +} + +/// Implement this trait to efficiently load entities based on +/// the entity manifest. +/// This entity loader is called "Simple" for two reasons: +/// 1) First, it is not synchronous- `load_entity` is called multiple times. +/// 2) Second, it is not precise- the entity manifest only requires some +/// fields to be loaded. +pub trait SimpleEntityLoader { + /// Simple entity loaders must implement `load_entity`, + /// a function that loads an entities based on their EntityUIDs. + /// For each element of `entity_ids`, returns the corresponding + /// [`Entity`] in the output vector. + fn load_entity(&mut self, entity_ids: &[&EntityUid]) -> Vec; + + /// Loads all the entities needed for a request + /// using the `load_entity` function. + fn load( + &mut self, + entity_manifest: &EntityManifest, + request: &Request, + ) -> Result { + Ok(Entities(entity_slicing::load_entities_simplified( + &entity_manifest, + &request.0, + &mut |entity_id| { + let uids = entity_id + .into_iter() + .map(|euid| EntityUid::from_str(&euid.to_string()).unwrap()) + .collect::>(); + self.load_entity(uids.iter().collect::>().as_slice()) + .into_iter() + .map(|ele| ele.0) + .collect() + }, + )?)) + } +} diff --git a/cedar-testing/src/cedar_test_impl.rs b/cedar-testing/src/cedar_test_impl.rs index 7a41e823f4..98438cde1c 100644 --- a/cedar-testing/src/cedar_test_impl.rs +++ b/cedar-testing/src/cedar_test_impl.rs @@ -20,6 +20,7 @@ //! testing (see ). pub use cedar_policy::ffi; +use cedar_policy::{compute_entity_manifest, compute_entity_slice_manifest}; use cedar_policy_core::ast::{self, PartialValue}; use cedar_policy_core::ast::{Expr, PolicySet, Request, Value}; use cedar_policy_core::authorizer::Authorizer; @@ -158,6 +159,7 @@ pub trait CedarTestImplementation { /// Custom authorizer entry point. fn is_authorized( &self, + schema: &ValidatorSchema, request: &Request, policies: &PolicySet, entities: &Entities, @@ -282,6 +284,7 @@ where impl CedarTestImplementation for RustEngine { fn is_authorized( &self, + schema: &ValidatorSchema, request: &Request, policies: &PolicySet, entities: &Entities, @@ -313,6 +316,18 @@ impl CedarTestImplementation for RustEngine { response, timing_info: HashMap::from([("authorize".into(), Micros(duration.as_micros()))]), }; + + // now check that we get the same response with entity manifest, as long as the schema is valid + let validator = Validator::new(schema.clone()); + let validation_result = validator.validate(policies, ValidationMode::Strict); + if validation_result.validation_passed() { + let entity_manifest = compute_entity_slice_manifest(&schema, &policies).unwrap(); + let entity_slice = entity_manifest.slice_entities(entities, request).unwrap(); + + let slice_response = authorizer.is_authorized(request.clone(), policies, &entity_slice); + assert_eq!(response.response.decision(), slice_response.decision); + } + TestResult::Success(response) } diff --git a/cedar-testing/src/integration_testing.rs b/cedar-testing/src/integration_testing.rs index ad6d9d5ad5..d2d9f5b2cf 100644 --- a/cedar-testing/src/integration_testing.rs +++ b/cedar-testing/src/integration_testing.rs @@ -292,7 +292,7 @@ pub fn perform_integration_test( for json_request in requests { let request = parse_request_from_test(&json_request, &schema, test_name); let response = test_impl - .is_authorized(&request, &policies, &entities) + .is_authorized(&schema, &request, &policies, &entities) .expect("Authorization failed"); // check decision assert_eq!( From 649649c4760a9f5fe242ffbf003504d5d172ccb2 Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 29 Jul 2024 15:39:05 -0700 Subject: [PATCH 02/56] fix up imports and such Signed-off-by: oflatt --- cedar-policy-validator/src/typecheck.rs | 30 ------------------- .../src/typecheck/test/test_utils.rs | 30 +++++++++++++++++++ cedar-policy/src/api.rs | 4 +-- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/cedar-policy-validator/src/typecheck.rs b/cedar-policy-validator/src/typecheck.rs index f055d81146..c2fd17b8e9 100644 --- a/cedar-policy-validator/src/typecheck.rs +++ b/cedar-policy-validator/src/typecheck.rs @@ -2306,33 +2306,3 @@ impl<'a> Typechecker<'a> { } } } - -impl Typechecker<'_> { - /// Typecheck an expression outside the context of a policy. This is - /// currently only used for testing. - pub(crate) fn typecheck_expr<'a>( - &self, - e: &'a Expr, - unique_type_errors: &mut HashSet, - ) -> TypecheckAnswer<'a> { - // Using bogus entity type names here for testing. They'll be treated as - // having empty attribute records, so tests will behave as expected. - let request_env = RequestEnv::DeclaredAction { - principal: &"Principal" - .parse() - .expect("Placeholder type \"Principal\" failed to parse as valid type name."), - action: &EntityUID::with_eid_and_type(ACTION_ENTITY_TYPE, "action") - .expect("ACTION_ENTITY_TYPE failed to parse as type name."), - resource: &"Resource" - .parse() - .expect("Placeholder type \"Resource\" failed to parse as valid type name."), - context: &Type::record_with_attributes(None, OpenTag::ClosedAttributes), - principal_slot: None, - resource_slot: None, - }; - let mut type_errors = Vec::new(); - let ans = self.typecheck(&request_env, &EffectSet::new(), e, &mut type_errors); - unique_type_errors.extend(type_errors); - ans - } -} diff --git a/cedar-policy-validator/src/typecheck/test/test_utils.rs b/cedar-policy-validator/src/typecheck/test/test_utils.rs index 56cb14b704..603466cc16 100644 --- a/cedar-policy-validator/src/typecheck/test/test_utils.rs +++ b/cedar-policy-validator/src/typecheck/test/test_utils.rs @@ -78,6 +78,36 @@ impl Type { } } +impl Typechecker<'_> { + /// Typecheck an expression outside the context of a policy. This is + /// currently only used for testing. + pub(crate) fn typecheck_expr<'a>( + &self, + e: &'a Expr, + unique_type_errors: &mut HashSet, + ) -> TypecheckAnswer<'a> { + // Using bogus entity type names here for testing. They'll be treated as + // having empty attribute records, so tests will behave as expected. + let request_env = RequestEnv::DeclaredAction { + principal: &"Principal" + .parse() + .expect("Placeholder type \"Principal\" failed to parse as valid type name."), + action: &EntityUID::with_eid_and_type(ACTION_ENTITY_TYPE, "action") + .expect("ACTION_ENTITY_TYPE failed to parse as type name."), + resource: &"Resource" + .parse() + .expect("Placeholder type \"Resource\" failed to parse as valid type name."), + context: &Type::record_with_attributes(None, OpenTag::ClosedAttributes), + principal_slot: None, + resource_slot: None, + }; + let mut type_errors = Vec::new(); + let ans = self.typecheck(&request_env, &CapabilitySet::new(), e, &mut type_errors); + unique_type_errors.extend(type_errors); + ans + } +} + /// Assert expected == actual by by asserting expected <: actual && actual <: expected. /// In the future it might better to only assert actual <: expected to allow /// improvement to the typechecker to return more specific types. diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index e94a031b52..5eb2c3096e 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -36,7 +36,7 @@ pub use ast::Effect; pub use authorizer::Decision; #[cfg(feature = "partial-eval")] use cedar_policy_core::ast::BorrowedRestrictedExpr; -use cedar_policy_core::ast::{self, EntityUID}; +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}; @@ -4294,7 +4294,7 @@ pub fn compute_entity_manifest( /// fields to be loaded. pub trait SimpleEntityLoader { /// Simple entity loaders must implement `load_entity`, - /// a function that loads an entities based on their EntityUIDs. + /// a function that loads an entities based on their [`EntityUID`]s. /// For each element of `entity_ids`, returns the corresponding /// [`Entity`] in the output vector. fn load_entity(&mut self, entity_ids: &[&EntityUid]) -> Vec; From fff1e1da4306af88fd7840f2e580edb356ef49ea Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 29 Jul 2024 15:45:07 -0700 Subject: [PATCH 03/56] remove unecessary functions for now Signed-off-by: oflatt --- cedar-policy-validator/src/entity_slicing.rs | 608 +------------------ 1 file changed, 1 insertion(+), 607 deletions(-) diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index e825290656..de27e77671 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -181,77 +181,6 @@ fn union_fields(first: &Fields, second: &Fields) -> Fields { res } -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_concrete_env() - .ok_or(EntitySliceError::PartialRequestError)?; - self.per_action - .get(&request_type) - .map(|primary| primary.slice_entities(entities, request)) - .unwrap_or(Ok(Entities::default())) - } - - /// Flatten this manifest into a [`FlatEntityManifest`] - pub fn to_flat_entity_manifest(&self) -> FlatEntityManifest { - let mut per_action: FlatPerAction = Default::default(); - - for (action, primary) in &self.per_action { - per_action.insert(action.clone(), primary.to_flat_primary_slice()); - } - FlatEntityManifest { per_action } - } - - /// Convert this manifest into a human-readable format. - /// The format specifies the request types, then - /// prints all the flattened paths as cedar expressions. - pub fn to_str_natural(&self) -> String { - let flattened = self.to_flat_entity_manifest(); - let exprs = flattened.to_exprs(); - let mut res = String::new(); - for (types, exprs) in exprs { - res.push_str(&format!("{} {{\n", types.to_str_natural())); - for expr in exprs { - res.push_str(&format!(" {}\n", expr)); - } - res.push_str("}\n"); - } - res - } -} - -impl FlatEntityManifest { - /// Convert this flattened manifest into a list of cedar expressions. - /// The expressions only use the constructors [`ExprKind::GetAttr`] and [`Var`]. - /// They may also have the shape `principal in expr` to denote that - /// the parents of `expr` are needed. - pub fn to_exprs(&self) -> HashMap> { - let mut res: HashMap> = Default::default(); - for (types, path) in &self.per_action { - let exprs = path.to_exprs(); - res.insert(types.clone(), exprs); - } - res - } -} - -impl FlatPrimarySlice { - fn to_exprs(&self) -> Vec { - let mut res = vec![]; - for slice in &self.data { - let expr = slice.to_expr(); - res.push(expr); - } - res - } -} - impl FlatEntitySlice { /// Given a path of fields to access, convert to a tree /// (the [`Fields`] data structure. @@ -282,68 +211,6 @@ impl FlatEntitySlice { primary_map.insert(self.root.clone(), current); PrimarySlice { trie: primary_map } } - - fn to_expr(&self) -> Expr { - let mut expr = self.root.to_expr(); - - for field in &self.path { - expr = Expr::get_attr(expr, field.clone()); - } - - if self.parents_required { - expr = Expr::binary_app(BinaryOp::In, Expr::var(Var::Principal), expr); - } - expr - } - - /// Converts compatible expressions to a [`FlatEntitySlice`] - /// Compatible expressions start with a variable, with - /// any number of [`ExprKin::GetAttr`], and optionally are wrapped with - /// `principal in ` - fn from_expr(expr: &Expr) -> Option { - let (mut current_expr, parents_required) = match expr.expr_kind() { - ExprKind::BinaryApp { - op: BinaryOp::In, - arg1, - arg2, - } => { - if **arg1 != Expr::var(Var::Principal) { - return None; - } - (arg2.clone(), true) - } - _ => (Arc::new(expr.clone()), false), - }; - - let mut path = vec![]; - loop { - match current_expr.expr_kind() { - ExprKind::GetAttr { expr, attr } => { - path.push(attr.clone()); - current_expr = expr.clone(); - } - ExprKind::Var(var) => { - path.reverse(); - - return Some(FlatEntitySlice { - root: EntityRoot::Var(*var), - path, - parents_required, - }); - } - ExprKind::Lit(Literal::EntityUID(literal)) => { - path.reverse(); - - return Some(FlatEntitySlice { - root: EntityRoot::Literal((**literal).clone()), - path, - parents_required, - }); - } - _ => return None, - } - } - } } impl EntityRoot { @@ -358,78 +225,6 @@ impl EntityRoot { } impl PrimarySlice { - /// 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(EntitySliceError::PartialRequestError)?; - slice.slice_entity(entities, entity_id, &mut res)?; - } - EntityRoot::Var(Var::Principal) => { - let entity_id = request - .principal() - .uid() - .ok_or(EntitySliceError::PartialRequestError)?; - slice.slice_entity(entities, entity_id, &mut res)?; - } - EntityRoot::Var(Var::Resource) => { - let resource_id = request - .resource() - .uid() - .ok_or(EntitySliceError::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(EntitySliceError::PartialRequestError)? - .clone(), - ); - let PartialValue::Value(val) = partial_val else { - return Err(EntitySliceError::PartialRequestError); - }; - slice.slice_val(entities, &val, &mut res); - } - } - } - } - Ok(Entities::from_entities( - res.into_values(), - None::<&NoEntitiesSchema>, - TCComputation::AssumeAlreadyComputed, - Extensions::all_available(), - )?) - } - - fn to_flat_primary_slice(&self) -> FlatPrimarySlice { - let mut data: Vec = vec![]; - - for (root, slice) in &self.trie { - for flattened in slice.to_flat_entity_slice(root) { - data.push(flattened); - } - } - - FlatPrimarySlice { data } - } - /// Create an empty [`PrimarySlice`] that requires no data pub fn new() -> Self { Self { @@ -473,91 +268,6 @@ impl EntitySlice { } impl EntitySlice { - /// 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(EntitySliceError::PartialEntity); - }; - let sliced = slice.slice_val(entities, &val, res)?; - - new_entity.insert(field.clone(), PartialValue::Value(sliced)); - } - } - - let new_ancestors = if self.parents_required { - entity.ancestors().cloned().collect() - } else { - HashSet::new() - }; - - let new_entity = - Entity::new_with_attr_partial_value(lit.clone(), new_entity, new_ancestors); - - #[allow(clippy::expect_used)] - if let Some(existing) = res.get_mut(lit) { - // Here we union the new entity with any existing one - // PANIC SAFETY: Entities in the entity store with the same ID should be compatible to union together. - *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.parents_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(EntitySliceError::IncompatibleEntityManifest(val.clone())); - } - - 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) - } - }) - } - fn new() -> Self { Self { children: Default::default(), @@ -565,39 +275,6 @@ impl EntitySlice { data: (), } } - - fn to_flat_entity_slice(&self, root: &EntityRoot) -> Vec { - let mut flattened_reversed = self.flatten_reversed(root); - for flattened in flattened_reversed.iter_mut() { - flattened.path.reverse(); - } - flattened_reversed - } - - /// Builds a [`FlatEntitySlice`] in reversed order for efficient - /// `push` operation. - fn flatten_reversed(&self, root: &EntityRoot) -> Vec { - if self.children.is_empty() { - vec![FlatEntitySlice { - root: root.clone(), - path: vec![], - parents_required: false, - }] - } else { - let mut res = vec![]; - for (key, value) in &self.children { - for mut flattened in value.flatten_reversed(root) { - if flattened.parents_required { - res.push(flattened.clone()); - } - flattened.path.push(key.clone()); - flattened.parents_required = self.parents_required; - res.push(flattened); - } - } - res - } - } } /// Computes an [`EntitySliceManifest`] from the schema and policies @@ -1025,68 +702,10 @@ fn find_remaining_entities_value( #[cfg(test)] mod entity_slice_tests { - use cedar_policy_core::{ast::{Context, PolicyID}, entities::EntityJsonParser, parser::parse_policy}; - - use crate::CoreSchema; + use cedar_policy_core::{ast::PolicyID, parser::parse_policy}; use super::*; - 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(); - - #[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(); @@ -1158,44 +777,6 @@ action Read appliesTo { }"#; let expected_manifest = serde_json::from_str(expected).unwrap(); assert_eq!(entity_manifest, expected_manifest); - - 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, - &expected_manifest, - ); } #[test] @@ -1247,49 +828,6 @@ action Read appliesTo { }"#; let expected_manifest = serde_json::from_str(expected).unwrap(); assert_eq!(entity_manifest, expected_manifest); - - 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, - &expected_manifest, - ); } #[test] @@ -1340,34 +878,6 @@ action Read appliesTo { }"#; let expected_manifest = serde_json::from_str(expected).unwrap(); assert_eq!(entity_manifest, expected_manifest); - - 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, - &expected_manifest, - ); } #[test] @@ -1442,60 +952,6 @@ action Read appliesTo { }"#; let expected_manifest = serde_json::from_str(expected).unwrap(); assert_eq!(entity_manifest, expected_manifest); - - 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, - &expected_manifest, - ); } #[test] @@ -1719,68 +1175,6 @@ action Read appliesTo { }"#; let expected_manifest = serde_json::from_str(expected).unwrap(); assert_eq!(entity_manifest, expected_manifest); - - 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, - &expected_manifest, - ); } #[test] From 9aad9f99246d47812e19cced1359943d5e9a69a2 Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 29 Jul 2024 15:45:40 -0700 Subject: [PATCH 04/56] revert test_impl.rs Signed-off-by: oflatt --- cedar-testing/src/cedar_test_impl.rs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/cedar-testing/src/cedar_test_impl.rs b/cedar-testing/src/cedar_test_impl.rs index 98438cde1c..1a98ea6892 100644 --- a/cedar-testing/src/cedar_test_impl.rs +++ b/cedar-testing/src/cedar_test_impl.rs @@ -20,8 +20,7 @@ //! testing (see ). pub use cedar_policy::ffi; -use cedar_policy::{compute_entity_manifest, compute_entity_slice_manifest}; -use cedar_policy_core::ast::{self, PartialValue}; +use cedar_policy_core::ast::PartialValue; use cedar_policy_core::ast::{Expr, PolicySet, Request, Value}; use cedar_policy_core::authorizer::Authorizer; use cedar_policy_core::entities::{Entities, TCComputation}; @@ -159,7 +158,6 @@ pub trait CedarTestImplementation { /// Custom authorizer entry point. fn is_authorized( &self, - schema: &ValidatorSchema, request: &Request, policies: &PolicySet, entities: &Entities, @@ -284,7 +282,6 @@ where impl CedarTestImplementation for RustEngine { fn is_authorized( &self, - schema: &ValidatorSchema, request: &Request, policies: &PolicySet, entities: &Entities, @@ -316,18 +313,6 @@ impl CedarTestImplementation for RustEngine { response, timing_info: HashMap::from([("authorize".into(), Micros(duration.as_micros()))]), }; - - // now check that we get the same response with entity manifest, as long as the schema is valid - let validator = Validator::new(schema.clone()); - let validation_result = validator.validate(policies, ValidationMode::Strict); - if validation_result.validation_passed() { - let entity_manifest = compute_entity_slice_manifest(&schema, &policies).unwrap(); - let entity_slice = entity_manifest.slice_entities(entities, request).unwrap(); - - let slice_response = authorizer.is_authorized(request.clone(), policies, &entity_slice); - assert_eq!(response.response.decision(), slice_response.decision); - } - TestResult::Success(response) } From b7495f1eb8342f57a105db8e9a5e137bce8e10b2 Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 29 Jul 2024 15:49:09 -0700 Subject: [PATCH 05/56] remove human format for manifest Signed-off-by: oflatt --- cedar-policy-cli/src/lib.rs | 9 +--- cedar-policy-validator/src/entity_slicing.rs | 55 +------------------- cedar-testing/src/integration_testing.rs | 2 +- 3 files changed, 5 insertions(+), 61 deletions(-) diff --git a/cedar-policy-cli/src/lib.rs b/cedar-policy-cli/src/lib.rs index 98ec167989..8ca55f4b38 100644 --- a/cedar-policy-cli/src/lib.rs +++ b/cedar-policy-cli/src/lib.rs @@ -158,9 +158,7 @@ pub enum SchemaFormat { #[derive(Debug, Clone, Copy, ValueEnum)] pub enum ManifestFormat { - /// Human-readable format - Human, - /// JSON format + /// JSON format- only one for now Json, } @@ -191,7 +189,7 @@ pub struct EntityManifestArgs { /// Schema format (Human-readable or JSON) #[arg(long, value_enum, default_value_t = SchemaFormat::Human)] pub schema_format: SchemaFormat, - #[arg(long, value_enum, default_value_t = ManifestFormat::Human)] + #[arg(long, value_enum, default_value_t = ManifestFormat::Json)] /// Manifest format (Human-readable or JSON) pub manifest_format: ManifestFormat, } @@ -748,9 +746,6 @@ pub fn entity_manifest(args: &EntityManifestArgs) -> CedarExitCode { } }; match args.manifest_format { - ManifestFormat::Human => { - println!("{}", manifest.to_str_natural()); - } ManifestFormat::Json => { println!("{}", serde_json::to_string_pretty(&manifest).unwrap()); } diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index de27e77671..9bde4fdd3b 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -1,12 +1,12 @@ //! Entity Slicing -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::hash::RandomState; use std::sync::Arc; use cedar_policy_core::entities::err::EntitiesError; -use cedar_policy_core::entities::{Dereference, NoEntitiesSchema, TCComputation}; +use cedar_policy_core::entities::{NoEntitiesSchema, TCComputation}; use cedar_policy_core::extensions::Extensions; use cedar_policy_core::{ ast::{ @@ -779,57 +779,6 @@ action Read appliesTo { assert_eq!(entity_manifest, expected_manifest); } - #[test] - #[should_panic] - 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 = ValidatorSchema::from_str_natural( - " -entity User = { - name: String, -}; - -entity Document; - -action Read appliesTo { - principal: [User], - resource: [Document] -}; - ", - Extensions::all_available(), - ) - .unwrap() - .0; - - let entity_manifest = - compute_entity_slice_manifest(&schema, &pset).expect("Should succeed"); - let expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "Read" - }, - "resource": "Document" - }, - { - "trie": [ - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).unwrap(); - assert_eq!(entity_manifest, expected_manifest); - } - #[test] fn test_empty_entity_manifest() { let mut pset = PolicySet::new(); diff --git a/cedar-testing/src/integration_testing.rs b/cedar-testing/src/integration_testing.rs index d2d9f5b2cf..ad6d9d5ad5 100644 --- a/cedar-testing/src/integration_testing.rs +++ b/cedar-testing/src/integration_testing.rs @@ -292,7 +292,7 @@ pub fn perform_integration_test( for json_request in requests { let request = parse_request_from_test(&json_request, &schema, test_name); let response = test_impl - .is_authorized(&schema, &request, &policies, &entities) + .is_authorized(&request, &policies, &entities) .expect("Authorization failed"); // check decision assert_eq!( From d8278334fa8c354af76f1f793b00a98018859d94 Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 29 Jul 2024 15:56:40 -0700 Subject: [PATCH 06/56] more clean up Signed-off-by: oflatt --- cedar-policy-validator/src/types.rs | 16 +++++------ cedar-policy/src/api.rs | 43 ++--------------------------- 2 files changed, 11 insertions(+), 48 deletions(-) diff --git a/cedar-policy-validator/src/types.rs b/cedar-policy-validator/src/types.rs index b202a1f35e..b9d5357906 100644 --- a/cedar-policy-validator/src/types.rs +++ b/cedar-policy-validator/src/types.rs @@ -23,7 +23,7 @@ mod request_env; pub use request_env::*; use itertools::Itertools; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use smol_str::SmolStr; use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, @@ -46,7 +46,7 @@ use crate::{validation_errors::LubHelp, ValidationMode}; use super::schema::{ValidatorActionId, ValidatorEntityType, ValidatorSchema}; /// The main type structure. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize)] pub enum Type { /// Bottom type. Sub-type of all types. Never, @@ -763,7 +763,7 @@ impl TryFrom for CoreSchemaType { /// Represents the least upper bound of multiple entity types. This can be used /// to represent the least upper bound of a single entity type, in which case it /// is exactly that entity type. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize)] pub struct EntityLUB { /// We store `EntityType` here because these are entity types. /// As of this writing, `EntityType` is backed by `Name` (rather than @@ -900,7 +900,7 @@ impl EntityLUB { /// Represents the attributes of a record or entity type. Each attribute has an /// identifier, a flag indicating weather it is required, and a type. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Default, Deserialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Default)] pub struct Attributes { /// Attributes map pub attrs: BTreeMap, @@ -1052,7 +1052,7 @@ impl IntoIterator for Attributes { /// Used to tag record types to indicate if their attributes record is open or /// closed. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Copy, Clone, Serialize)] pub enum OpenTag { /// The attributes are open. A value of this type may have attributes other /// than those listed. @@ -1075,7 +1075,7 @@ impl OpenTag { /// /// The subtyping lattice for these types is that /// `Entity` <: `AnyEntity`. `Record` does not subtype anything. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize)] pub enum EntityRecordKind { /// A record type, with these attributes Record { @@ -1355,7 +1355,7 @@ impl EntityRecordKind { } /// Contains the type of a record attribute and if the attribute is required. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AttributeType { /// The type of the attribute. @@ -1384,7 +1384,7 @@ impl AttributeType { } /// Represent the possible primitive types. -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Debug, Clone, Serialize)] pub enum Primitive { /// Primitive boolean type. Bool, diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 5eb2c3096e..9203277ca8 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -23,10 +23,11 @@ )] mod id; -use cedar_policy_validator::typecheck::{PolicyCheck, Typechecker}; +use cedar_policy_validator::entity_slicing::compute_entity_slice_manifest; pub use cedar_policy_validator::entity_slicing::{ - self, compute_entity_slice_manifest, EntityManifest, EntitySliceError, + EntityManifest, EntitySlice, EntitySliceError, PrimarySlice, }; +use cedar_policy_validator::typecheck::{PolicyCheck, Typechecker}; pub use id::*; mod err; @@ -4275,7 +4276,6 @@ action CreateList in Create appliesTo { } } - /// Given a schema and policy set, compute an entity slice manifest. /// The manifest describes the data required to answer requests /// for each action type. @@ -4285,40 +4285,3 @@ pub fn compute_entity_manifest( ) -> Result { compute_entity_slice_manifest(&schema.0, &pset.ast) } - -/// Implement this trait to efficiently load entities based on -/// the entity manifest. -/// This entity loader is called "Simple" for two reasons: -/// 1) First, it is not synchronous- `load_entity` is called multiple times. -/// 2) Second, it is not precise- the entity manifest only requires some -/// fields to be loaded. -pub trait SimpleEntityLoader { - /// Simple entity loaders must implement `load_entity`, - /// a function that loads an entities based on their [`EntityUID`]s. - /// For each element of `entity_ids`, returns the corresponding - /// [`Entity`] in the output vector. - fn load_entity(&mut self, entity_ids: &[&EntityUid]) -> Vec; - - /// Loads all the entities needed for a request - /// using the `load_entity` function. - fn load( - &mut self, - entity_manifest: &EntityManifest, - request: &Request, - ) -> Result { - Ok(Entities(entity_slicing::load_entities_simplified( - &entity_manifest, - &request.0, - &mut |entity_id| { - let uids = entity_id - .into_iter() - .map(|euid| EntityUid::from_str(&euid.to_string()).unwrap()) - .collect::>(); - self.load_entity(uids.iter().collect::>().as_slice()) - .into_iter() - .map(|ele| ele.0) - .collect() - }, - )?)) - } -} From d2a750755b5cc4e141578595e8e557bf6dc4cb13 Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 29 Jul 2024 15:57:58 -0700 Subject: [PATCH 07/56] rename Signed-off-by: oflatt --- cedar-policy-validator/src/entity_slicing.rs | 58 ++++++++++---------- cedar-policy/src/api.rs | 2 +- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index 9bde4fdd3b..155d8f53ca 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -28,7 +28,7 @@ use crate::{ ValidationMode, ValidatorSchema, }; -type PerAction = HashMap>; +type PerAction = HashMap>; type FlatPerAction = HashMap; /// Data structure that tells the user what data is needed @@ -53,7 +53,7 @@ pub struct FlatEntityManifest { } /// A map of data fields to entity slices -pub type Fields = HashMap>>; +pub type Fields = HashMap>>; /// The root of an entity slice. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] @@ -76,14 +76,14 @@ impl Display for EntityRoot { /// a [`PrimarySlice`] is a tree that tells you what data to load #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct PrimarySlice +pub struct RequestEntityManifest where T: Clone, { #[serde_as(as = "Vec<(_, _)>")] #[serde(bound(deserialize = "T: Default"))] /// The data that needs to be loaded, organized by root - pub trie: HashMap>, + pub trie: HashMap>, } /// A flattened version of a [`PrimarySlice`] @@ -96,7 +96,7 @@ pub struct FlatPrimarySlice { /// An entity slice- tells users a tree of data to load #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct EntitySlice { +pub struct AccessTrie { /// Child data of this entity slice. #[serde_as(as = "Vec<(_, _)>")] pub children: Fields, @@ -186,21 +186,21 @@ impl FlatEntitySlice { /// (the [`Fields`] data structure. /// Also, when we need to pull all the data for the final field /// do so. - fn to_primary_slice(&self) -> PrimarySlice { - self.to_primary_slice_with_leaf(EntitySlice { + fn to_primary_slice(&self) -> RequestEntityManifest { + self.to_primary_slice_with_leaf(AccessTrie { parents_required: true, children: Default::default(), data: (), }) } - fn to_primary_slice_with_leaf(&self, leaf_entity: EntitySlice) -> PrimarySlice { + fn to_primary_slice_with_leaf(&self, leaf_entity: AccessTrie) -> RequestEntityManifest { let mut current = leaf_entity; // reverse the path, visiting the last access first for field in self.path.iter().rev() { let mut fields = HashMap::new(); fields.insert(field.clone(), Box::new(current)); - current = EntitySlice { + current = AccessTrie { parents_required: false, children: fields, data: (), @@ -209,7 +209,7 @@ impl FlatEntitySlice { let mut primary_map = HashMap::new(); primary_map.insert(self.root.clone(), current); - PrimarySlice { trie: primary_map } + RequestEntityManifest { trie: primary_map } } } @@ -224,7 +224,7 @@ impl EntityRoot { } } -impl PrimarySlice { +impl RequestEntityManifest { /// Create an empty [`PrimarySlice`] that requires no data pub fn new() -> Self { Self { @@ -233,7 +233,7 @@ impl PrimarySlice { } } -impl PrimarySlice { +impl RequestEntityManifest { /// Union two [`PrimarySlice`]s together, requiring /// the data that both of them require fn union(&self, other: &Self) -> Self { @@ -249,13 +249,13 @@ impl PrimarySlice { } } -impl Default for PrimarySlice { +impl Default for RequestEntityManifest { fn default() -> Self { Self::new() } } -impl EntitySlice { +impl AccessTrie { /// Union two [`EntitySlice`]s together, requiring /// the data that both of them require fn union(&self, other: &Self) -> Self { @@ -267,7 +267,7 @@ impl EntitySlice { } } -impl EntitySlice { +impl AccessTrie { fn new() -> Self { Self { children: Default::default(), @@ -282,7 +282,7 @@ pub fn compute_entity_slice_manifest( schema: &ValidatorSchema, policies: &PolicySet, ) -> Result { - let mut manifest: HashMap = HashMap::new(); + let mut manifest: HashMap = HashMap::new(); // now, for each policy we add the data it requires to the manifest for policy in policies.policies() { @@ -297,7 +297,7 @@ pub fn compute_entity_slice_manifest( // always results in false, // so we need no data - Ok(PrimarySlice::new()) + Ok(RequestEntityManifest::new()) } // TODO is returning the first error correct? // Also, should we run full validation instead of just @@ -330,14 +330,16 @@ pub fn compute_entity_slice_manifest( }) } -fn compute_primary_slice(expr: &Expr>) -> Result { - let mut primary_slice = PrimarySlice::new(); +fn compute_primary_slice( + expr: &Expr>, +) -> Result { + let mut primary_slice = RequestEntityManifest::new(); add_to_primary_slice(&mut primary_slice, expr, false)?; Ok(primary_slice) } fn add_to_primary_slice( - primary_slice: &mut PrimarySlice, + primary_slice: &mut RequestEntityManifest, expr: &Expr>, should_load_all: bool, ) -> Result<(), EntitySliceError> { @@ -438,7 +440,7 @@ fn add_to_primary_slice( .expect("Typechecked expression missing type"), ) } else { - EntitySlice::new() + AccessTrie::new() }; *primary_slice = flat_slice.to_primary_slice_with_leaf(leaf_field); @@ -473,7 +475,7 @@ fn full_tree_for_entity_or_record(ty: &EntityRecordKind) -> Fields<()> { } } -fn entity_slice_from_type(ty: &Type) -> EntitySlice { +fn entity_slice_from_type(ty: &Type) -> AccessTrie { match ty { // if it's not an entity or record, slice ends here Type::ExtensionType { .. } @@ -481,8 +483,8 @@ fn entity_slice_from_type(ty: &Type) -> EntitySlice { | Type::True | Type::False | Type::Primitive { .. } - | Type::Set { .. } => EntitySlice::new(), - Type::EntityOrRecord(record_type) => EntitySlice { + | Type::Set { .. } => AccessTrie::new(), + Type::EntityOrRecord(record_type) => AccessTrie { children: full_tree_for_entity_or_record(record_type), parents_required: false, data: (), @@ -613,7 +615,7 @@ fn load_entity_slice( loader: &mut impl FnMut(&[&EntityUID]) -> Vec, entities: &mut HashMap, entity: &EntityUID, - slice: &EntitySlice, + slice: &AccessTrie, ) -> Result<(), EntitySliceError> { // special case: no need to load anything for empty fields with no parents required if slice.children.is_empty() && !slice.parents_required { @@ -652,7 +654,7 @@ fn load_entity_slice( pub fn find_remaining_entities( entity: &Entity, fields: &Fields<()>, -) -> Result, EntitySliceError> { +) -> Result, EntitySliceError> { let mut remaining = HashMap::new(); for (field, slice) in fields { if let Some(pvalue) = entity.get(field) { @@ -667,9 +669,9 @@ pub fn find_remaining_entities( } fn find_remaining_entities_value( - remaining: &mut HashMap, + remaining: &mut HashMap, value: &Value, - slice: &EntitySlice, + slice: &AccessTrie, ) -> Result<(), EntitySliceError> { match value.value_kind() { ValueKind::Lit(literal) => match literal { diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 9203277ca8..4b20b71f28 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -25,7 +25,7 @@ mod id; use cedar_policy_validator::entity_slicing::compute_entity_slice_manifest; pub use cedar_policy_validator::entity_slicing::{ - EntityManifest, EntitySlice, EntitySliceError, PrimarySlice, + AccessTrie, EntityManifest, EntitySliceError, RequestEntityManifest, }; use cedar_policy_validator::typecheck::{PolicyCheck, Typechecker}; pub use id::*; From 5564cfcfc3bebb9cd361d41e6a67564d6754e4bc Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 29 Jul 2024 16:02:03 -0700 Subject: [PATCH 08/56] more removal of code Signed-off-by: oflatt --- cedar-policy-core/Cargo.toml | 2 - cedar-policy-core/src/ast/entity.rs | 46 -------------------- cedar-policy-core/src/ast/partial_value.rs | 14 ------ cedar-policy-core/src/ast/request.rs | 16 +------ cedar-policy-core/src/ast/value.rs | 29 ------------ cedar-policy-core/src/entities.rs | 28 ------------ cedar-policy-validator/Cargo.toml | 2 - cedar-policy-validator/src/entity_slicing.rs | 2 +- cedar-policy/Cargo.toml | 1 - 9 files changed, 2 insertions(+), 138 deletions(-) diff --git a/cedar-policy-core/Cargo.toml b/cedar-policy-core/Cargo.toml index aec811de08..390dff3820 100644 --- a/cedar-policy-core/Cargo.toml +++ b/cedar-policy-core/Cargo.toml @@ -28,8 +28,6 @@ stacker = "0.1.15" arbitrary = { version = "1", features = ["derive"], optional = true } miette = { version = "7.1.0", features = ["serde"] } nonempty = "0.10.0" -aws-sdk-dynamodb = "1.32.0" -futures = "0.3.1" # decimal extension requires regex regex = { version = "1.8", features = ["unicode"], optional = true } diff --git a/cedar-policy-core/src/ast/entity.rs b/cedar-policy-core/src/ast/entity.rs index 38be60363c..08f3f850af 100644 --- a/cedar-policy-core/src/ast/entity.rs +++ b/cedar-policy-core/src/ast/entity.rs @@ -318,49 +318,6 @@ 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, @@ -465,9 +422,6 @@ impl Entity { } } - /// Test if two `Entity` objects are deep/structurally equal. - /// That is, not only do they have the same UID, but also the same - /// attributes, attribute values, and ancestors. pub(crate) fn deep_eq(&self, other: &Self) -> bool { self.uid == other.uid && self.attrs == other.attrs && self.ancestors == other.ancestors } diff --git a/cedar-policy-core/src/ast/partial_value.rs b/cedar-policy-core/src/ast/partial_value.rs index 23408d556f..86bf119c96 100644 --- a/cedar-policy-core/src/ast/partial_value.rs +++ b/cedar-policy-core/src/ast/partial_value.rs @@ -31,20 +31,6 @@ 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/request.rs b/cedar-policy-core/src/ast/request.rs index e0c6af6b70..fcd43d0696 100644 --- a/cedar-policy-core/src/ast/request.rs +++ b/cedar-policy-core/src/ast/request.rs @@ -61,20 +61,6 @@ pub struct RequestType { pub resource: EntityType, } -impl RequestType { - /// Create a human-readable string for the request types, - /// written as a tuple of three elements. - /// TODO would be better as a cedar struct? - pub fn to_str_natural(&self) -> String { - format!( - "({} {} {})", - self.principal.name(), - self.action, - self.resource.name(), - ) - } -} - /// An entry in a request for a Entity UID. /// It may either be a concrete EUID /// or an unknown in the case of partial evaluation @@ -217,7 +203,7 @@ impl Request { /// This includes the types of the principal, action, resource, /// and context. /// Returns `None` if the request is not fully concrete. - pub fn to_concrete_env(&self) -> Option { + pub fn to_request_type(&self) -> Option { Some(RequestType { principal: self.principal.uid()?.clone().components().0, action: self.action.uid()?.clone(), diff --git a/cedar-policy-core/src/ast/value.rs b/cedar-policy-core/src/ast/value.rs index 72835e5aed..7f81c4e262 100644 --- a/cedar-policy-core/src/ast/value.rs +++ b/cedar-policy-core/src/ast/value.rs @@ -68,35 +68,6 @@ 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 bb733f8c97..a8a3cfe32f 100644 --- a/cedar-policy-core/src/entities.rs +++ b/cedar-policy-core/src/entities.rs @@ -72,34 +72,6 @@ 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/Cargo.toml b/cedar-policy-validator/Cargo.toml index 4b6667ff3d..a8f3f00b95 100644 --- a/cedar-policy-validator/Cargo.toml +++ b/cedar-policy-validator/Cargo.toml @@ -27,8 +27,6 @@ arbitrary = { version = "1", features = ["derive"], optional = true } lalrpop-util = { version = "0.20.0", features = ["lexer", "unicode"] } lazy_static = "1.4.0" nonempty = "0.10.0" -aws-sdk-dynamodb = "1.32.0" -futures = "0.3.1" # wasm dependencies serde-wasm-bindgen = { version = "0.6", optional = true } diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index 155d8f53ca..6e3332ab57 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -543,7 +543,7 @@ pub fn load_entities_simplified( ) -> Result { let Some(primary_slice) = manifest.per_action.get( &request - .to_concrete_env() + .to_request_type() .ok_or(EntitySliceError::PartialRequestError)?, ) else { // if the request type isn't in the manifest, we need no data diff --git a/cedar-policy/Cargo.toml b/cedar-policy/Cargo.toml index d5148edda6..94a182c777 100644 --- a/cedar-policy/Cargo.toml +++ b/cedar-policy/Cargo.toml @@ -26,7 +26,6 @@ smol_str = { version = "0.2", features = ["serde"] } dhat = { version = "0.3.2", optional = true } serde_with = "3.3.0" nonempty = "0.10" -aws-sdk-dynamodb = "1.32.0" # wasm dependencies serde-wasm-bindgen = { version = "0.6", optional = true } From f2e456d644bf4300a8edaa490a4f52565251ba02 Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 29 Jul 2024 16:05:00 -0700 Subject: [PATCH 09/56] more cleanup and rename Signed-off-by: oflatt --- Cargo.toml | 2 +- cedar-policy-core/src/ast/entity.rs | 3 +++ cedar-policy-validator/src/entity_slicing.rs | 28 +++++++++----------- cedar-policy-validator/src/err.rs | 7 +---- cedar-policy-validator/src/typecheck.rs | 2 +- cedar-policy/src/api.rs | 2 +- 6 files changed, 20 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3d8b991ced..97627ff61a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ "cedar-policy-formatter", "cedar-policy-cli", "cedar-testing", - "cedar-wasm", + "cedar-wasm" ] resolver = "2" diff --git a/cedar-policy-core/src/ast/entity.rs b/cedar-policy-core/src/ast/entity.rs index 08f3f850af..1408748fb9 100644 --- a/cedar-policy-core/src/ast/entity.rs +++ b/cedar-policy-core/src/ast/entity.rs @@ -422,6 +422,9 @@ impl Entity { } } + /// Test if two `Entity` objects are deep/structurally equal. + /// That is, not only do they have the same UID, but also the same + /// attributes, attribute values, and ancestors. pub(crate) fn deep_eq(&self, other: &Self) -> bool { self.uid == other.uid && self.attrs == other.attrs && self.ancestors == other.ancestors } diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index 6e3332ab57..da052567a8 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -28,7 +28,7 @@ use crate::{ ValidationMode, ValidatorSchema, }; -type PerAction = HashMap>; +type PerAction = HashMap>; type FlatPerAction = HashMap; /// Data structure that tells the user what data is needed @@ -76,7 +76,7 @@ impl Display for EntityRoot { /// a [`PrimarySlice`] is a tree that tells you what data to load #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RequestEntityManifest +pub struct RootAccessTrie where T: Clone, { @@ -186,7 +186,7 @@ impl FlatEntitySlice { /// (the [`Fields`] data structure. /// Also, when we need to pull all the data for the final field /// do so. - fn to_primary_slice(&self) -> RequestEntityManifest { + fn to_primary_slice(&self) -> RootAccessTrie { self.to_primary_slice_with_leaf(AccessTrie { parents_required: true, children: Default::default(), @@ -194,7 +194,7 @@ impl FlatEntitySlice { }) } - fn to_primary_slice_with_leaf(&self, leaf_entity: AccessTrie) -> RequestEntityManifest { + fn to_primary_slice_with_leaf(&self, leaf_entity: AccessTrie) -> RootAccessTrie { let mut current = leaf_entity; // reverse the path, visiting the last access first for field in self.path.iter().rev() { @@ -209,7 +209,7 @@ impl FlatEntitySlice { let mut primary_map = HashMap::new(); primary_map.insert(self.root.clone(), current); - RequestEntityManifest { trie: primary_map } + RootAccessTrie { trie: primary_map } } } @@ -224,7 +224,7 @@ impl EntityRoot { } } -impl RequestEntityManifest { +impl RootAccessTrie { /// Create an empty [`PrimarySlice`] that requires no data pub fn new() -> Self { Self { @@ -233,7 +233,7 @@ impl RequestEntityManifest { } } -impl RequestEntityManifest { +impl RootAccessTrie { /// Union two [`PrimarySlice`]s together, requiring /// the data that both of them require fn union(&self, other: &Self) -> Self { @@ -249,7 +249,7 @@ impl RequestEntityManifest { } } -impl Default for RequestEntityManifest { +impl Default for RootAccessTrie { fn default() -> Self { Self::new() } @@ -282,7 +282,7 @@ pub fn compute_entity_slice_manifest( schema: &ValidatorSchema, policies: &PolicySet, ) -> Result { - let mut manifest: HashMap = HashMap::new(); + let mut manifest: HashMap = HashMap::new(); // now, for each policy we add the data it requires to the manifest for policy in policies.policies() { @@ -297,7 +297,7 @@ pub fn compute_entity_slice_manifest( // always results in false, // so we need no data - Ok(RequestEntityManifest::new()) + Ok(RootAccessTrie::new()) } // TODO is returning the first error correct? // Also, should we run full validation instead of just @@ -330,16 +330,14 @@ pub fn compute_entity_slice_manifest( }) } -fn compute_primary_slice( - expr: &Expr>, -) -> Result { - let mut primary_slice = RequestEntityManifest::new(); +fn compute_primary_slice(expr: &Expr>) -> Result { + let mut primary_slice = RootAccessTrie::new(); add_to_primary_slice(&mut primary_slice, expr, false)?; Ok(primary_slice) } fn add_to_primary_slice( - primary_slice: &mut RequestEntityManifest, + primary_slice: &mut RootAccessTrie, expr: &Expr>, should_load_all: bool, ) -> Result<(), EntitySliceError> { diff --git a/cedar-policy-validator/src/err.rs b/cedar-policy-validator/src/err.rs index f6e74463e1..54df6195b9 100644 --- a/cedar-policy-validator/src/err.rs +++ b/cedar-policy-validator/src/err.rs @@ -14,6 +14,7 @@ * limitations under the License. */ +use crate::cedar_schema; use cedar_policy_core::{ ast::{EntityUID, ReservedNameError}, transitive_closure, @@ -22,12 +23,6 @@ use itertools::{Either, Itertools}; use miette::Diagnostic; use nonempty::NonEmpty; use thiserror::Error; -<<<<<<< HEAD - -use crate::cedar_schema; -======= -use crate::human_schema; ->>>>>>> b9d23316 (Entity slicing implementation) /// Error creating a schema from the Cedar syntax #[derive(Debug, Error, Diagnostic)] diff --git a/cedar-policy-validator/src/typecheck.rs b/cedar-policy-validator/src/typecheck.rs index c2fd17b8e9..e720f8e4ca 100644 --- a/cedar-policy-validator/src/typecheck.rs +++ b/cedar-policy-validator/src/typecheck.rs @@ -40,7 +40,7 @@ use crate::{ use cedar_policy_core::ast::{ BinaryOp, EntityType, EntityUID, Expr, ExprBuilder, ExprKind, Literal, Name, PolicyID, - PrincipalOrResourceConstraint, SlotId, Template, UnaryOp, Var, ACTION_ENTITY_TYPE, + PrincipalOrResourceConstraint, SlotId, Template, UnaryOp, Var, }; #[cfg(not(target_arch = "wasm32"))] diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 4b20b71f28..6cea71855e 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -25,7 +25,7 @@ mod id; use cedar_policy_validator::entity_slicing::compute_entity_slice_manifest; pub use cedar_policy_validator::entity_slicing::{ - AccessTrie, EntityManifest, EntitySliceError, RequestEntityManifest, + AccessTrie, EntityManifest, EntitySliceError, RootAccessTrie, }; use cedar_policy_validator::typecheck::{PolicyCheck, Typechecker}; pub use id::*; From e7d41d94823a3bd3d2265823e0447a6050caca0c Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 29 Jul 2024 16:07:32 -0700 Subject: [PATCH 10/56] remove more simple api stuff Signed-off-by: oflatt --- cedar-policy-validator/src/entity_slicing.rs | 207 +------------------ 1 file changed, 6 insertions(+), 201 deletions(-) diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index da052567a8..c8a78004ea 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -2,19 +2,12 @@ use std::collections::HashMap; use std::fmt::{Display, Formatter}; -use std::hash::RandomState; use std::sync::Arc; -use cedar_policy_core::entities::err::EntitiesError; -use cedar_policy_core::entities::{NoEntitiesSchema, TCComputation}; -use cedar_policy_core::extensions::Extensions; -use cedar_policy_core::{ - ast::{ - BinaryOp, Entity, EntityUID, Expr, ExprKind, Literal, PartialValue, PolicySet, Request, - RequestType, UnaryOp, Value, ValueKind, Var, - }, - entities::Entities, +use cedar_policy_core::ast::{ + BinaryOp, EntityUID, Expr, ExprKind, Literal, PolicySet, RequestType, UnaryOp, Var, }; +use cedar_policy_core::entities::err::EntitiesError; use miette::Diagnostic; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -149,24 +142,6 @@ pub enum EntitySliceError { /// TODO make a more specific error that includes the expression #[error("Failed to analyze policy: mixed getting attributes with other operators")] FailedAnalysis, - - /// During entity loading, failed to find an entity. - #[error("Missing entity `{0}` during entity loading.")] - MissingEntity(EntityUID), - - /// During entity loading, attempted to load from - /// a type without fields. - #[error("Expected entity or record during entity loading. Got value: {0}")] - IncompatibleEntityManifest(Value), - - /// Found a partial entity during entity loading. - #[error("Found partial entity while doing entity slicing.")] - PartialEntity, - - /// During entity loading using the simplified API, - /// the entity loader returned the wrong number of entities. - #[error("Wrong number of entities returned ({0}). Expected {1}.")] - WrongNumberOfEntities(usize, usize), } fn union_fields(first: &Fields, second: &Fields) -> Fields { @@ -299,9 +274,10 @@ pub fn compute_entity_slice_manifest( Ok(RootAccessTrie::new()) } + // TODO is returning the first error correct? // Also, should we run full validation instead of just - // typechecking? + // typechecking? Validation does a little more right? PolicyCheck::Fail(errors) => { // PANIC SAFETY policy check fail // should be a non-empty vector. @@ -529,180 +505,9 @@ fn get_expr_path(expr: &Expr>) -> Result Vec, -) -> Result { - let Some(primary_slice) = manifest.per_action.get( - &request - .to_request_type() - .ok_or(EntitySliceError::PartialRequestError)?, - ) else { - // if the request type isn't in the manifest, we need no data - return Entities::from_entities( - vec![], - None::<&NoEntitiesSchema>, - TCComputation::AssumeAlreadyComputed, - Extensions::all_available(), - ) - .map_err(|err| err.into()); - }; - - let mut entities: HashMap = Default::default(); - - for (key, value) in &primary_slice.trie { - match key { - EntityRoot::Var(Var::Principal) => { - load_entity_slice( - loader, - &mut entities, - request - .principal() - .uid() - .ok_or(EntitySliceError::PartialRequestError)?, - value, - )?; - } - EntityRoot::Var(Var::Action) => { - load_entity_slice( - loader, - &mut entities, - request - .action() - .uid() - .ok_or(EntitySliceError::PartialRequestError)?, - value, - )?; - } - EntityRoot::Var(Var::Resource) => { - load_entity_slice( - loader, - &mut entities, - request - .resource() - .uid() - .ok_or(EntitySliceError::PartialRequestError)?, - value, - )?; - } - EntityRoot::Literal(lit) => { - load_entity_slice(loader, &mut entities, lit, value)?; - } - EntityRoot::Var(Var::Context) => { - // skip context, since the simplified loader assumes the entire context is loaded - } - } - } - - Entities::from_entities( - entities.values().cloned(), - None::<&NoEntitiesSchema>, - TCComputation::AssumeAlreadyComputed, - Extensions::all_available(), - ) - .map_err(|err| err.into()) -} - -fn load_entity_slice( - loader: &mut impl FnMut(&[&EntityUID]) -> Vec, - entities: &mut HashMap, - entity: &EntityUID, - slice: &AccessTrie, -) -> Result<(), EntitySliceError> { - // special case: no need to load anything for empty fields with no parents required - if slice.children.is_empty() && !slice.parents_required { - return Ok(()); - } - - let new_entities = loader(&[entity]); - if new_entities.len() != 1 { - return Err(EntitySliceError::WrongNumberOfEntities( - 1, - new_entities.len(), - )); - } - #[allow(clippy::expect_used)] - let new_entity = new_entities - .into_iter() - .next() - .expect("Vector has length 1 as shown by if statement above."); - - // now we need to load any entity references - let remaining_entities = find_remaining_entities(&new_entity, &slice.children); - - for (id, slice) in remaining_entities? { - load_entity_slice(loader, entities, &id, &slice)?; - } - - // TODO also need to load parents of some entities - - entities.insert(new_entity.uid().clone(), new_entity); - Ok(()) -} - -/// This helper function finds all entity references that need to be -/// loaded given an already-loaded [`Entity`] and corresponding [`Fields`]. -/// Returns pairs of entity and slices that need to be loaded. -pub fn find_remaining_entities( - entity: &Entity, - fields: &Fields<()>, -) -> Result, EntitySliceError> { - let mut remaining = HashMap::new(); - for (field, slice) in fields { - if let Some(pvalue) = entity.get(field) { - let PartialValue::Value(value) = pvalue else { - return Err(EntitySliceError::PartialEntity); - }; - find_remaining_entities_value(&mut remaining, value, slice)?; - } - } - - Ok(remaining) -} - -fn find_remaining_entities_value( - remaining: &mut HashMap, - value: &Value, - slice: &AccessTrie, -) -> Result<(), EntitySliceError> { - match value.value_kind() { - ValueKind::Lit(literal) => match literal { - Literal::EntityUID(entity_id) => { - if let Some(existing) = remaining.get_mut(entity_id) { - *existing = existing.union(slice); - } else { - remaining.insert((**entity_id).clone(), slice.clone()); - } - } - _ => assert!(slice.children.is_empty()), - }, - ValueKind::Set(_) => { - assert!(slice.children.is_empty()); - } - ValueKind::ExtensionValue(_) => { - assert!(slice.children.is_empty()); - } - ValueKind::Record(record) => { - for (field, child_slice) in &slice.children { - // only need to slice if field is present - if let Some(value) = record.get(field) { - find_remaining_entities_value(remaining, value, child_slice)?; - } - } - } - }; - Ok(()) -} - #[cfg(test)] mod entity_slice_tests { - use cedar_policy_core::{ast::PolicyID, parser::parse_policy}; + use cedar_policy_core::{ast::PolicyID, extensions::Extensions, parser::parse_policy}; use super::*; From 9bf73e5e5b3645bf4607202807979265256b6390 Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 29 Jul 2024 16:36:10 -0700 Subject: [PATCH 11/56] better error message for non-analyzable policies Signed-off-by: oflatt --- cedar-policy-core/src/ast/expr.rs | 24 +++++ cedar-policy-validator/src/entity_slicing.rs | 108 +++++++++++++------ 2 files changed, 97 insertions(+), 35 deletions(-) diff --git a/cedar-policy-core/src/ast/expr.rs b/cedar-policy-core/src/ast/expr.rs index e1449666a0..866fe3a91e 100644 --- a/cedar-policy-core/src/ast/expr.rs +++ b/cedar-policy-core/src/ast/expr.rs @@ -180,6 +180,30 @@ impl From for Expr { } } +impl ExprKind { + /// Describe this operator for error messages. + pub fn operator_description(self: ExprKind) -> String { + match self { + ExprKind::Lit(_) => "literal".to_string(), + ExprKind::Var(_) => "variable".to_string(), + ExprKind::Slot(_) => "slot".to_string(), + ExprKind::Unknown(_) => "unknown".to_string(), + ExprKind::If { .. } => "if".to_string(), + ExprKind::And { .. } => "&&".to_string(), + ExprKind::Or { .. } => "||".to_string(), + ExprKind::UnaryApp { op, .. } => op.to_string(), + ExprKind::BinaryApp { op, .. } => op.to_string(), + ExprKind::ExtensionFunctionApp { fn_name, .. } => fn_name.to_string(), + ExprKind::GetAttr { .. } => "get attribute".to_string(), + ExprKind::HasAttr { .. } => "has attribute".to_string(), + ExprKind::Like { .. } => "like".to_string(), + ExprKind::Is { .. } => "is".to_string(), + ExprKind::Set(_) => "set".to_string(), + ExprKind::Record(_) => "record".to_string(), + } + } +} + impl Expr { fn new(expr_kind: ExprKind, source_loc: Option, data: T) -> Self { Self { diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index c8a78004ea..2d3899f2ea 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -5,9 +5,11 @@ use std::fmt::{Display, Formatter}; use std::sync::Arc; use cedar_policy_core::ast::{ - BinaryOp, EntityUID, Expr, ExprKind, Literal, PolicySet, RequestType, UnaryOp, Var, + BinaryOp, EntityUID, Expr, ExprKind, Literal, PolicyID, PolicySet, RequestType, UnaryOp, Var, }; use cedar_policy_core::entities::err::EntitiesError; +use cedar_policy_core::impl_diagnostic_from_source_loc_field; +use cedar_policy_core::parser::Loc; use miette::Diagnostic; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -115,6 +117,28 @@ pub struct FlatEntitySlice { pub parents_required: bool, } +#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)] +#[error("For policy `{policy_id}`, failed to analyze expression while computing entity manifest.`")] +struct FailedAnalysisError { + /// Source location + pub source_loc: Option, + /// Policy ID where the error occurred + pub policy_id: PolicyID, + /// The kind of the expression that was unexpected + pub expr_kind: ExprKind, +} + +impl Diagnostic for FailedAnalysisError { + impl_diagnostic_from_source_loc_field!(); + + fn help<'a>(&'a self) -> Option> { + Some(Box::new(format!( + "Entity slicing failed to analyze expression: {} operators are not allowed before accessing record or entity attributes.", + self.expr_kind.operator_description() + ))) + } +} + /// An error generated by entity slicing. #[derive(Debug, Error, Diagnostic)] #[non_exhaustive] @@ -137,11 +161,11 @@ pub enum EntitySliceError { )] PartialExpressionError, - /// A policy was not analyzable because it used operators + /// A policy was not analyzable because it used unsupported operators /// before a [`ExprKind::GetAttr`] - /// TODO make a more specific error that includes the expression - #[error("Failed to analyze policy: mixed getting attributes with other operators")] - FailedAnalysis, + #[error(transparent)] + #[diagnostic(transparent)] + FailedAnalysis(#[from] FailedAnalysisError), } fn union_fields(first: &Fields, second: &Fields) -> Fields { @@ -267,7 +291,9 @@ pub fn compute_entity_slice_manifest( for (request_env, policy_check) in request_envs { // match on the typechecking answer let new_primary_slice = match policy_check { - PolicyCheck::Success(typechecked_expr) => compute_primary_slice(&typechecked_expr), + PolicyCheck::Success(typechecked_expr) => { + compute_primary_slice(&typechecked_expr, policy.id()) + } PolicyCheck::Irrelevant(_) => { // always results in false, // so we need no data @@ -306,15 +332,19 @@ pub fn compute_entity_slice_manifest( }) } -fn compute_primary_slice(expr: &Expr>) -> Result { +fn compute_primary_slice( + expr: &Expr>, + policy_id: PolicyID, +) -> Result { let mut primary_slice = RootAccessTrie::new(); - add_to_primary_slice(&mut primary_slice, expr, false)?; + add_to_primary_slice(&mut primary_slice, expr, policy_id, false)?; Ok(primary_slice) } fn add_to_primary_slice( primary_slice: &mut RootAccessTrie, expr: &Expr>, + policy_id: PolicyID, should_load_all: bool, ) -> Result<(), EntitySliceError> { match expr.expr_kind() { @@ -329,46 +359,46 @@ fn add_to_primary_slice( then_expr, else_expr, } => { - add_to_primary_slice(primary_slice, test_expr, should_load_all)?; - add_to_primary_slice(primary_slice, then_expr, should_load_all)?; - add_to_primary_slice(primary_slice, else_expr, should_load_all)?; + add_to_primary_slice(primary_slice, test_expr, policy_id, should_load_all)?; + add_to_primary_slice(primary_slice, then_expr, policy_id, should_load_all)?; + add_to_primary_slice(primary_slice, else_expr, policy_id, should_load_all)?; } ExprKind::And { left, right } => { - add_to_primary_slice(primary_slice, left, should_load_all)?; - add_to_primary_slice(primary_slice, right, should_load_all)?; + add_to_primary_slice(primary_slice, left, policy_id, should_load_all)?; + add_to_primary_slice(primary_slice, right, policy_id, should_load_all)?; } ExprKind::Or { left, right } => { - add_to_primary_slice(primary_slice, left, should_load_all)?; - add_to_primary_slice(primary_slice, right, should_load_all)?; + add_to_primary_slice(primary_slice, left, policy_id, should_load_all)?; + add_to_primary_slice(primary_slice, right, policy_id, should_load_all)?; } // For unary and binary operations, we need to be careful // to remain sound. // For example, equality requires that we pull all data ExprKind::UnaryApp { op, arg } => match op { - UnaryOp::Not => add_to_primary_slice(primary_slice, arg, should_load_all)?, - UnaryOp::Neg => add_to_primary_slice(primary_slice, arg, should_load_all)?, + UnaryOp::Not => add_to_primary_slice(primary_slice, arg, policy_id, should_load_all)?, + UnaryOp::Neg => add_to_primary_slice(primary_slice, arg, policy_id, should_load_all)?, }, ExprKind::BinaryApp { op, arg1, arg2 } => match op { BinaryOp::Eq => { - add_to_primary_slice(primary_slice, arg1, true)?; - add_to_primary_slice(primary_slice, arg1, true)?; + add_to_primary_slice(primary_slice, arg1, policy_id, true)?; + add_to_primary_slice(primary_slice, arg1, policy_id, true)?; } BinaryOp::In => { // add arg2 to primary slice - add_to_primary_slice(primary_slice, arg2, should_load_all)?; + add_to_primary_slice(primary_slice, arg2, policy_id, should_load_all)?; // get the path for arg1 - let mut flat_slice = get_expr_path(arg1)?; + let mut flat_slice = get_expr_path(arg1, policy_id)?; flat_slice.parents_required = true; *primary_slice = primary_slice.union(&flat_slice.to_primary_slice()); } BinaryOp::Contains | BinaryOp::ContainsAll | BinaryOp::ContainsAny => { - add_to_primary_slice(primary_slice, arg1, true)?; - add_to_primary_slice(primary_slice, arg2, true)?; + add_to_primary_slice(primary_slice, arg1, policy_id, true)?; + add_to_primary_slice(primary_slice, arg2, policy_id, true)?; } BinaryOp::Less | BinaryOp::LessEq | BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul => { - add_to_primary_slice(primary_slice, arg1, should_load_all)?; - add_to_primary_slice(primary_slice, arg2, should_load_all)?; + add_to_primary_slice(primary_slice, arg1, policy_id, should_load_all)?; + add_to_primary_slice(primary_slice, arg2, policy_id, should_load_all)?; } }, ExprKind::ExtensionFunctionApp { fn_name: _, args } => { @@ -376,35 +406,35 @@ fn add_to_primary_slice( // don't take full structs as inputs. // If they did, we would need to use logic similar to the Eq binary operator. for arg in args.iter() { - add_to_primary_slice(primary_slice, arg, should_load_all)?; + add_to_primary_slice(primary_slice, arg, policy_id, should_load_all)?; } } ExprKind::Like { expr, pattern: _ } => { - add_to_primary_slice(primary_slice, expr, should_load_all)?; + add_to_primary_slice(primary_slice, expr, policy_id, should_load_all)?; } ExprKind::Is { expr, entity_type: _, } => { - add_to_primary_slice(primary_slice, expr, should_load_all)?; + add_to_primary_slice(primary_slice, expr, policy_id, should_load_all)?; } ExprKind::Set(contents) => { for expr in &**contents { - add_to_primary_slice(primary_slice, expr, should_load_all)?; + add_to_primary_slice(primary_slice, expr, policy_id, should_load_all)?; } } ExprKind::Record(content) => { for expr in content.values() { - add_to_primary_slice(primary_slice, expr, should_load_all)?; + add_to_primary_slice(primary_slice, expr, policy_id, should_load_all)?; } } ExprKind::HasAttr { expr, attr } => { - let mut flat_slice = get_expr_path(expr)?; + let mut flat_slice = get_expr_path(expr, policy_id)?; flat_slice.path.push(attr.clone()); *primary_slice = primary_slice.union(&flat_slice.to_primary_slice()); } ExprKind::GetAttr { .. } => { - let flat_slice = get_expr_path(expr)?; + let flat_slice = get_expr_path(expr, policy_id)?; #[allow(clippy::expect_used)] let leaf_field = if should_load_all { @@ -468,7 +498,10 @@ fn entity_slice_from_type(ty: &Type) -> AccessTrie { /// Given an expression, get the corresponding data path /// starting with a variable. -fn get_expr_path(expr: &Expr>) -> Result { +fn get_expr_path( + expr: &Expr>, + policy_id: PolicyID, +) -> Result { Ok(match expr.expr_kind() { ExprKind::Slot(slot_id) => { if slot_id.is_principal() { @@ -492,7 +525,7 @@ fn get_expr_path(expr: &Expr>) -> Result { - let mut slice = get_expr_path(expr)?; + let mut slice = get_expr_path(expr, policy_id)?; slice.path.push(attr.clone()); slice } @@ -501,7 +534,12 @@ fn get_expr_path(expr: &Expr>) -> Result Err(EntitySliceError::FailedAnalysis)?, + ExprKind::Unknown(_) => Err(EntitySliceError::PartialExpressionError)?, + _ => Err(EntitySliceError::FailedAnalysis(FailedAnalysisError { + source_loc: expr.source_loc().cloned(), + policy_id, + expr_kind: expr.expr_kind(), + }))?, }) } From 1a9b8d262a40f671a859901aca9fb0fc175793f0 Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 29 Jul 2024 16:43:13 -0700 Subject: [PATCH 12/56] better error message and comment Signed-off-by: oflatt --- cedar-policy-core/src/ast/expr.rs | 2 +- cedar-policy-validator/src/entity_slicing.rs | 31 +++++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/cedar-policy-core/src/ast/expr.rs b/cedar-policy-core/src/ast/expr.rs index 866fe3a91e..7377347612 100644 --- a/cedar-policy-core/src/ast/expr.rs +++ b/cedar-policy-core/src/ast/expr.rs @@ -182,7 +182,7 @@ impl From for Expr { impl ExprKind { /// Describe this operator for error messages. - pub fn operator_description(self: ExprKind) -> String { + pub fn operator_description(self: &ExprKind) -> String { match self { ExprKind::Lit(_) => "literal".to_string(), ExprKind::Var(_) => "variable".to_string(), diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index 2d3899f2ea..e7184b8238 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -117,15 +117,32 @@ pub struct FlatEntitySlice { pub parents_required: bool, } +/// Entity manifest computation does not handle the full +/// cedar language. In particular, the policies must follow the +/// following grammar: +/// = +/// in +/// + +/// if { } { } +/// ... all other cedar operators not mentioned by datapath-expr + +/// = . +/// has +/// +/// +/// +/// The `get_expr_path` function handles `datapath-expr` expressions. +/// This error message tells the user not to use certain operators +/// before accessing record or entity attributes, breaking this grammar. #[derive(Debug, Clone, Error, Hash, Eq, PartialEq)] #[error("For policy `{policy_id}`, failed to analyze expression while computing entity manifest.`")] -struct FailedAnalysisError { +pub struct FailedAnalysisError { /// Source location pub source_loc: Option, /// Policy ID where the error occurred pub policy_id: PolicyID, /// The kind of the expression that was unexpected - pub expr_kind: ExprKind, + pub expr_kind: ExprKind>, } impl Diagnostic for FailedAnalysisError { @@ -334,7 +351,7 @@ pub fn compute_entity_slice_manifest( fn compute_primary_slice( expr: &Expr>, - policy_id: PolicyID, + policy_id: &PolicyID, ) -> Result { let mut primary_slice = RootAccessTrie::new(); add_to_primary_slice(&mut primary_slice, expr, policy_id, false)?; @@ -344,7 +361,7 @@ fn compute_primary_slice( fn add_to_primary_slice( primary_slice: &mut RootAccessTrie, expr: &Expr>, - policy_id: PolicyID, + policy_id: &PolicyID, should_load_all: bool, ) -> Result<(), EntitySliceError> { match expr.expr_kind() { @@ -500,7 +517,7 @@ fn entity_slice_from_type(ty: &Type) -> AccessTrie { /// starting with a variable. fn get_expr_path( expr: &Expr>, - policy_id: PolicyID, + policy_id: &PolicyID, ) -> Result { Ok(match expr.expr_kind() { ExprKind::Slot(slot_id) => { @@ -537,8 +554,8 @@ fn get_expr_path( ExprKind::Unknown(_) => Err(EntitySliceError::PartialExpressionError)?, _ => Err(EntitySliceError::FailedAnalysis(FailedAnalysisError { source_loc: expr.source_loc().cloned(), - policy_id, - expr_kind: expr.expr_kind(), + policy_id: policy_id.clone(), + expr_kind: expr.expr_kind().clone(), }))?, }) } From 56c92c061e582bf8fbf9fac2cfe2d2bbf48b58bb Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 30 Jul 2024 10:30:31 -0700 Subject: [PATCH 13/56] better comments Signed-off-by: oflatt --- cedar-policy-core/src/ast/request.rs | 5 ++-- cedar-policy-validator/src/entity_slicing.rs | 30 ++++++++++++-------- cedar-policy/src/api.rs | 2 ++ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/cedar-policy-core/src/ast/request.rs b/cedar-policy-core/src/ast/request.rs index fcd43d0696..87afdb47f6 100644 --- a/cedar-policy-core/src/ast/request.rs +++ b/cedar-policy-core/src/ast/request.rs @@ -200,8 +200,9 @@ impl Request { } /// Get the request types that correspond to this request. - /// This includes the types of the principal, action, resource, - /// and context. + /// This includes the types of the principal, action, and resource. + /// [`RequestType`] is used by the entity manifest. + /// The context type is implied by the action's type. /// Returns `None` if the request is not fully concrete. pub fn to_request_type(&self) -> Option { Some(RequestType { diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index e7184b8238..96b33d9382 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -23,28 +23,27 @@ use crate::{ ValidationMode, ValidatorSchema, }; -type PerAction = HashMap>; -type FlatPerAction = HashMap; - -/// Data structure that tells the user what data is needed -/// based on the action's ID +/// Data structure storing what data is needed +/// based on the the [`RequestType`]. +/// For each request type, the [`EntityManifest`] stores +/// a [`RootAccessTrie`] of data to retrieve. #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct EntityManifest where T: Clone, { - /// A map from actions to primary slice + /// A map from request types to [`RootAccessTrie`]s. #[serde_as(as = "Vec<(_, _)>")] #[serde(bound(deserialize = "T: Default"))] - pub per_action: PerAction, + pub per_action: HashMap>, } /// A flattened version of an [`EntityManifest`] #[derive(Debug)] pub struct FlatEntityManifest { /// For each action, all the data paths required - pub per_action: FlatPerAction, + pub per_action: HashMap, } /// A map of data fields to entity slices @@ -68,7 +67,12 @@ impl Display for EntityRoot { } } -/// a [`PrimarySlice`] is a tree that tells you what data to load +/// A [`RootAccessTrie`] is a trie describing +/// data paths to retrieve. Each edge in the trie +/// is either a record or entity dereference. +/// +/// If an entity or record field does not exist in the backing store, +/// it is safe to stop loading data at that point. #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RootAccessTrie @@ -77,7 +81,7 @@ where { #[serde_as(as = "Vec<(_, _)>")] #[serde(bound(deserialize = "T: Default"))] - /// The data that needs to be loaded, organized by root + /// The data that needs to be loaded, organized by root. pub trie: HashMap>, } @@ -130,7 +134,7 @@ pub struct FlatEntitySlice { /// has /// /// -/// +/// /// The `get_expr_path` function handles `datapath-expr` expressions. /// This error message tells the user not to use certain operators /// before accessing record or entity attributes, breaking this grammar. @@ -293,7 +297,9 @@ impl AccessTrie { } } -/// Computes an [`EntitySliceManifest`] from the schema and policies +/// Computes an [`EntitySliceManifest`] from the schema and policies. +/// The policies must validate against the schema in strict mode, +/// otherwise an error is returned. pub fn compute_entity_slice_manifest( schema: &ValidatorSchema, policies: &PolicySet, diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 6cea71855e..a6834a6be0 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -4277,6 +4277,8 @@ action CreateList in Create appliesTo { } /// Given a schema and policy set, compute an entity slice manifest. +/// The policies must validate against the schema in strict mode, +/// otherwise an error is returned. /// The manifest describes the data required to answer requests /// for each action type. pub fn compute_entity_manifest( From dcfc1cdb359ee98e34572d470296d1cc49be19a0 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 30 Jul 2024 12:22:57 -0700 Subject: [PATCH 14/56] much better docs Signed-off-by: oflatt --- cedar-policy-validator/src/entity_slicing.rs | 212 +++++++++--------- .../src/types/request_env.rs | 2 +- 2 files changed, 105 insertions(+), 109 deletions(-) diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index 96b33d9382..77e3608558 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::fmt::{Display, Formatter}; -use std::sync::Arc; use cedar_policy_core::ast::{ BinaryOp, EntityUID, Expr, ExprKind, Literal, PolicyID, PolicySet, RequestType, UnaryOp, Var, @@ -39,14 +38,9 @@ where pub per_action: HashMap>, } -/// A flattened version of an [`EntityManifest`] -#[derive(Debug)] -pub struct FlatEntityManifest { - /// For each action, all the data paths required - pub per_action: HashMap, -} - -/// A map of data fields to entity slices +/// A map of data fields to [`AccessTrie`]s. +/// The keys to this map form the edges in the access trie, +/// pointing to sub-tries. pub type Fields = HashMap>>; /// The root of an entity slice. @@ -85,18 +79,12 @@ where pub trie: HashMap>, } -/// A flattened version of a [`PrimarySlice`] -#[derive(Debug)] -pub struct FlatPrimarySlice { - /// All the paths of data required, each starting with a root [`Var`] - pub data: Vec, -} - /// An entity slice- tells users a tree of data to load #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 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<(_, _)>")] pub children: Fields, /// For entity types, this boolean may be `true` @@ -112,7 +100,7 @@ pub struct AccessTrie { /// A data path that may end with requesting the parents of /// an entity. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct FlatEntitySlice { +struct AccessPath { /// The root variable that begins the data path pub root: EntityRoot, /// The path of fields of entities or structs @@ -161,6 +149,8 @@ impl Diagnostic for FailedAnalysisError { } /// An error generated by entity slicing. +/// See [`FailedAnalysisError`] for details on the fragment +/// of Cedar handled by entity slicing. #[derive(Debug, Error, Diagnostic)] #[non_exhaustive] pub enum EntitySliceError { @@ -184,11 +174,13 @@ pub enum EntitySliceError { /// A policy was not analyzable because it used unsupported operators /// before a [`ExprKind::GetAttr`] + /// See [`FailedAnalysisError`] for more details. #[error(transparent)] #[diagnostic(transparent)] FailedAnalysis(#[from] FailedAnalysisError), } +/// Union two tries by combining the fields. fn union_fields(first: &Fields, second: &Fields) -> Fields { let mut res = first.clone(); for (key, value) in second { @@ -201,11 +193,8 @@ fn union_fields(first: &Fields, second: &Fields) -> Fields { res } -impl FlatEntitySlice { - /// Given a path of fields to access, convert to a tree - /// (the [`Fields`] data structure. - /// Also, when we need to pull all the data for the final field - /// do so. +impl AccessPath { + /// Convert a [`AccessPath`] into corresponding [`RootAccessTrie`]. fn to_primary_slice(&self) -> RootAccessTrie { self.to_primary_slice_with_leaf(AccessTrie { parents_required: true, @@ -214,8 +203,10 @@ impl FlatEntitySlice { }) } - fn to_primary_slice_with_leaf(&self, leaf_entity: AccessTrie) -> RootAccessTrie { - let mut current = leaf_entity; + /// Convert an [`AccessPath`] to a [`RootAccessTrie`], and also + /// add a full trie as the leaf at the end. + fn to_primary_slice_with_leaf(&self, leaf_trie: AccessTrie) -> RootAccessTrie { + let mut current = leaf_trie; // reverse the path, visiting the last access first for field in self.path.iter().rev() { let mut fields = HashMap::new(); @@ -233,19 +224,8 @@ impl FlatEntitySlice { } } -impl EntityRoot { - /// Convert this root to a cedar expression. - /// This will either be a variable or a literal. - pub fn to_expr(&self) -> Expr { - match self { - Self::Literal(lit) => Expr::val(Literal::EntityUID(Arc::new(lit.clone()))), - Self::Var(var) => Expr::var(*var), - } - } -} - impl RootAccessTrie { - /// Create an empty [`PrimarySlice`] that requires no data + /// Create an empty [`PrimarySlice`] that requests nothing. pub fn new() -> Self { Self { trie: Default::default(), @@ -254,8 +234,8 @@ impl RootAccessTrie { } impl RootAccessTrie { - /// Union two [`PrimarySlice`]s together, requiring - /// the data that both of them require + /// Union two [`RootAccessTrie`]s together. + /// The new trie requests the data from both of the original. fn union(&self, other: &Self) -> Self { let mut res = self.clone(); for (key, value) in &other.trie { @@ -276,8 +256,8 @@ impl Default for RootAccessTrie { } impl AccessTrie { - /// Union two [`EntitySlice`]s together, requiring - /// the data that both of them require + /// Union two [`AccessTrie`]s together. + /// The new trie requests the data from both of the original. fn union(&self, other: &Self) -> Self { Self { children: union_fields(&self.children, &other.children), @@ -288,6 +268,7 @@ impl AccessTrie { } impl AccessTrie { + /// A new trie that requests no data. fn new() -> Self { Self { children: Default::default(), @@ -315,12 +296,12 @@ pub fn compute_entity_slice_manifest( // match on the typechecking answer let new_primary_slice = match policy_check { PolicyCheck::Success(typechecked_expr) => { - compute_primary_slice(&typechecked_expr, policy.id()) + // compute the trie from the typechecked expr + // using static analysis + compute_root_trie(&typechecked_expr, policy.id()) } PolicyCheck::Irrelevant(_) => { - // always results in false, - // so we need no data - + // this policy is ireelevant, so we need no data Ok(RootAccessTrie::new()) } @@ -339,13 +320,14 @@ pub fn compute_entity_slice_manifest( } }?; - let request_types = request_env - .to_request_types() + let request_type = request_env + .to_request_type() .ok_or(EntitySliceError::PartialRequestError)?; - if let Some(existing) = manifest.get_mut(&request_types) { + // Add to the manifest based on the request type. + if let Some(existing) = manifest.get_mut(&request_type) { *existing = existing.union(&new_primary_slice); } else { - manifest.insert(request_types, new_primary_slice); + manifest.insert(request_type, new_primary_slice); } } } @@ -355,17 +337,23 @@ pub fn compute_entity_slice_manifest( }) } -fn compute_primary_slice( +/// A static analysis on type-annotated cedar expressions. +/// Computes the [`RootAccessTrie`] representing all the data required +/// to evaluate the expression. +fn compute_root_trie( expr: &Expr>, policy_id: &PolicyID, ) -> Result { let mut primary_slice = RootAccessTrie::new(); - add_to_primary_slice(&mut primary_slice, expr, policy_id, false)?; + add_to_root_trie(&mut primary_slice, expr, policy_id, false)?; Ok(primary_slice) } -fn add_to_primary_slice( - primary_slice: &mut RootAccessTrie, +/// Add the expression's requested data to the [`RootAccessTrie`]. +/// This handles s from the grammar (see [`FailedAnalysisError`]) +/// while [`get_expr_path`] handles the s. +fn add_to_root_trie( + root_trie: &mut RootAccessTrie, expr: &Expr>, policy_id: &PolicyID, should_load_all: bool, @@ -382,46 +370,49 @@ fn add_to_primary_slice( then_expr, else_expr, } => { - add_to_primary_slice(primary_slice, test_expr, policy_id, should_load_all)?; - add_to_primary_slice(primary_slice, then_expr, policy_id, should_load_all)?; - add_to_primary_slice(primary_slice, else_expr, policy_id, should_load_all)?; + add_to_root_trie(root_trie, test_expr, policy_id, should_load_all)?; + add_to_root_trie(root_trie, then_expr, policy_id, should_load_all)?; + add_to_root_trie(root_trie, else_expr, policy_id, should_load_all)?; } ExprKind::And { left, right } => { - add_to_primary_slice(primary_slice, left, policy_id, should_load_all)?; - add_to_primary_slice(primary_slice, right, policy_id, should_load_all)?; + add_to_root_trie(root_trie, left, policy_id, should_load_all)?; + add_to_root_trie(root_trie, right, policy_id, should_load_all)?; } ExprKind::Or { left, right } => { - add_to_primary_slice(primary_slice, left, policy_id, should_load_all)?; - add_to_primary_slice(primary_slice, right, policy_id, should_load_all)?; + add_to_root_trie(root_trie, left, policy_id, should_load_all)?; + add_to_root_trie(root_trie, right, policy_id, should_load_all)?; } - // For unary and binary operations, we need to be careful - // to remain sound. - // For example, equality requires that we pull all data ExprKind::UnaryApp { op, arg } => match op { - UnaryOp::Not => add_to_primary_slice(primary_slice, arg, policy_id, should_load_all)?, - UnaryOp::Neg => add_to_primary_slice(primary_slice, arg, policy_id, should_load_all)?, + UnaryOp::Not => add_to_root_trie(root_trie, arg, policy_id, should_load_all)?, + UnaryOp::Neg => add_to_root_trie(root_trie, arg, policy_id, should_load_all)?, }, ExprKind::BinaryApp { op, arg1, arg2 } => match op { + // Special case! Equality between records requires + // that we load all fields. + // This could be made more precise if we check the type. BinaryOp::Eq => { - add_to_primary_slice(primary_slice, arg1, policy_id, true)?; - add_to_primary_slice(primary_slice, arg1, policy_id, true)?; + add_to_root_trie(root_trie, arg1, policy_id, true)?; + add_to_root_trie(root_trie, arg1, policy_id, true)?; } BinaryOp::In => { - // add arg2 to primary slice - add_to_primary_slice(primary_slice, arg2, policy_id, should_load_all)?; + // Recur normally on the rhs + add_to_root_trie(root_trie, arg2, policy_id, should_load_all)?; - // get the path for arg1 + // The lhs should be a datapath expression. let mut flat_slice = get_expr_path(arg1, policy_id)?; flat_slice.parents_required = true; - *primary_slice = primary_slice.union(&flat_slice.to_primary_slice()); + *root_trie = root_trie.union(&flat_slice.to_primary_slice()); } BinaryOp::Contains | BinaryOp::ContainsAll | BinaryOp::ContainsAny => { - add_to_primary_slice(primary_slice, arg1, policy_id, true)?; - add_to_primary_slice(primary_slice, arg2, policy_id, true)?; + // Like equality, another special case for records. + add_to_root_trie(root_trie, arg1, policy_id, true)?; + add_to_root_trie(root_trie, arg2, policy_id, true)?; } BinaryOp::Less | BinaryOp::LessEq | BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul => { - add_to_primary_slice(primary_slice, arg1, policy_id, should_load_all)?; - add_to_primary_slice(primary_slice, arg2, policy_id, should_load_all)?; + // These operators work on values, so no special + // case is needed. + add_to_root_trie(root_trie, arg1, policy_id, should_load_all)?; + add_to_root_trie(root_trie, arg2, policy_id, should_load_all)?; } }, ExprKind::ExtensionFunctionApp { fn_name: _, args } => { @@ -429,39 +420,39 @@ fn add_to_primary_slice( // don't take full structs as inputs. // If they did, we would need to use logic similar to the Eq binary operator. for arg in args.iter() { - add_to_primary_slice(primary_slice, arg, policy_id, should_load_all)?; + add_to_root_trie(root_trie, arg, policy_id, should_load_all)?; } } ExprKind::Like { expr, pattern: _ } => { - add_to_primary_slice(primary_slice, expr, policy_id, should_load_all)?; + add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; } ExprKind::Is { expr, entity_type: _, } => { - add_to_primary_slice(primary_slice, expr, policy_id, should_load_all)?; + add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; } ExprKind::Set(contents) => { for expr in &**contents { - add_to_primary_slice(primary_slice, expr, policy_id, should_load_all)?; + add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; } } ExprKind::Record(content) => { for expr in content.values() { - add_to_primary_slice(primary_slice, expr, policy_id, should_load_all)?; + add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; } } ExprKind::HasAttr { expr, attr } => { let mut flat_slice = get_expr_path(expr, policy_id)?; flat_slice.path.push(attr.clone()); - *primary_slice = primary_slice.union(&flat_slice.to_primary_slice()); + *root_trie = root_trie.union(&flat_slice.to_primary_slice()); } ExprKind::GetAttr { .. } => { let flat_slice = get_expr_path(expr, policy_id)?; #[allow(clippy::expect_used)] let leaf_field = if should_load_all { - entity_slice_from_type( + type_to_access_trie( expr.data() .as_ref() .expect("Typechecked expression missing type"), @@ -470,14 +461,29 @@ fn add_to_primary_slice( AccessTrie::new() }; - *primary_slice = flat_slice.to_primary_slice_with_leaf(leaf_field); + *root_trie = flat_slice.to_primary_slice_with_leaf(leaf_field); } }; Ok(()) } -fn full_tree_for_entity_or_record(ty: &EntityRecordKind) -> Fields<()> { +/// Compute the full [`AccessTrie`] required for the type. +fn type_to_access_trie(ty: &Type) -> AccessTrie { + match ty { + // if it's not an entity or record, slice ends here + Type::ExtensionType { .. } + | Type::Never + | Type::True + | Type::False + | Type::Primitive { .. } + | Type::Set { .. } => AccessTrie::new(), + Type::EntityOrRecord(record_type) => entity_or_record_to_access_trie(record_type), + } +} + +/// Compute the full [`AccessTrie`] for the given entity or record type. +fn entity_or_record_to_access_trie(ty: &EntityRecordKind) -> AccessTrie { match ty { EntityRecordKind::ActionEntity { name: _, attrs } | EntityRecordKind::Record { @@ -488,61 +494,50 @@ fn full_tree_for_entity_or_record(ty: &EntityRecordKind) -> Fields<()> { for (attr_name, attr_type) in attrs.iter() { fields.insert( attr_name.clone(), - Box::new(entity_slice_from_type(&attr_type.attr_type)), + Box::new(type_to_access_trie(&attr_type.attr_type)), ); } - fields + AccessTrie { + children: fields, + parents_required: false, + data: (), + } } EntityRecordKind::Entity(_) | EntityRecordKind::AnyEntity => { // no need to load data for entities, which are compared // using ids - Default::default() + AccessTrie::new() } } } -fn entity_slice_from_type(ty: &Type) -> AccessTrie { - match ty { - // if it's not an entity or record, slice ends here - Type::ExtensionType { .. } - | Type::Never - | Type::True - | Type::False - | Type::Primitive { .. } - | Type::Set { .. } => AccessTrie::new(), - Type::EntityOrRecord(record_type) => AccessTrie { - children: full_tree_for_entity_or_record(record_type), - parents_required: false, - data: (), - }, - } -} - /// Given an expression, get the corresponding data path /// starting with a variable. +/// If the expression is not a ``, return an error. +/// See [`FailedAnalysisError`] for more information. fn get_expr_path( expr: &Expr>, policy_id: &PolicyID, -) -> Result { +) -> Result { Ok(match expr.expr_kind() { ExprKind::Slot(slot_id) => { if slot_id.is_principal() { - FlatEntitySlice { + AccessPath { root: EntityRoot::Var(Var::Principal), path: vec![], parents_required: false, } } else { assert!(slot_id.is_resource()); - FlatEntitySlice { + AccessPath { root: EntityRoot::Var(Var::Resource), path: vec![], parents_required: false, } } } - ExprKind::Var(var) => FlatEntitySlice { + ExprKind::Var(var) => AccessPath { root: EntityRoot::Var(*var), path: vec![], parents_required: false, @@ -552,12 +547,13 @@ fn get_expr_path( slice.path.push(attr.clone()); slice } - ExprKind::Lit(Literal::EntityUID(literal)) => FlatEntitySlice { + ExprKind::Lit(Literal::EntityUID(literal)) => AccessPath { root: EntityRoot::Literal((**literal).clone()), path: vec![], parents_required: false, }, ExprKind::Unknown(_) => Err(EntitySliceError::PartialExpressionError)?, + // all other variants of expressions result in failure to analyze. _ => Err(EntitySliceError::FailedAnalysis(FailedAnalysisError { source_loc: expr.source_loc().cloned(), policy_id: policy_id.clone(), diff --git a/cedar-policy-validator/src/types/request_env.rs b/cedar-policy-validator/src/types/request_env.rs index a9879ded1a..ab4f85c7b9 100644 --- a/cedar-policy-validator/src/types/request_env.rs +++ b/cedar-policy-validator/src/types/request_env.rs @@ -52,7 +52,7 @@ pub enum RequestEnv<'a> { impl<'a> RequestEnv<'a> { /// Return the types of each of the elements of this request. /// Returns [`None`] when the request is not fully concrete. - pub fn to_request_types(&self) -> Option { + pub fn to_request_type(&self) -> Option { match self { RequestEnv::DeclaredAction { principal, From 132ab4dac52d0e352e379865b38babcaf6bafcc5 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 30 Jul 2024 12:30:43 -0700 Subject: [PATCH 15/56] add experimental warning Signed-off-by: oflatt --- cedar-policy-validator/src/entity_slicing.rs | 5 +++++ cedar-policy/src/api.rs | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index 77e3608558..9b32b1ac6a 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -26,6 +26,7 @@ use crate::{ /// based on the the [`RequestType`]. /// For each request type, the [`EntityManifest`] stores /// a [`RootAccessTrie`] of data to retrieve. +#[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct EntityManifest @@ -41,9 +42,11 @@ where /// A map of data fields to [`AccessTrie`]s. /// The keys to this map form the edges in the access trie, /// pointing to sub-tries. +#[doc = include_str!("../../cedar-policy/experimental_warning.md")] pub type Fields = HashMap>>; /// The root of an entity slice. +#[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub enum EntityRoot { /// Literal entity ids @@ -67,6 +70,7 @@ impl Display for EntityRoot { /// /// If an entity or record field does not exist in the backing store, /// it is safe to stop loading data at that point. +#[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RootAccessTrie @@ -80,6 +84,7 @@ where } /// An entity slice- tells users a tree of data to load +#[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AccessTrie { diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index a6834a6be0..330c912b6b 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -25,7 +25,7 @@ mod id; use cedar_policy_validator::entity_slicing::compute_entity_slice_manifest; pub use cedar_policy_validator::entity_slicing::{ - AccessTrie, EntityManifest, EntitySliceError, RootAccessTrie, + AccessTrie, EntityManifest, EntityRoot, EntitySliceError, Fields, RootAccessTrie, }; use cedar_policy_validator::typecheck::{PolicyCheck, Typechecker}; pub use id::*; @@ -4281,6 +4281,7 @@ action CreateList in Create appliesTo { /// otherwise an error is returned. /// The manifest describes the data required to answer requests /// for each action type. +#[doc = include_str!("../experimental_warning.md")] pub fn compute_entity_manifest( schema: &Schema, pset: &PolicySet, From b5eca9b3c2e3400736bb3453b20e625d77880bf9 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 30 Jul 2024 12:37:41 -0700 Subject: [PATCH 16/56] update changelog, remove cli for now Signed-off-by: oflatt --- cedar-policy-cli/src/lib.rs | 57 -------------------- cedar-policy-cli/src/main.rs | 1 - cedar-policy-validator/src/entity_slicing.rs | 20 +++---- cedar-policy/CHANGELOG.md | 14 ++++- cedar-policy/src/api.rs | 4 +- 5 files changed, 22 insertions(+), 74 deletions(-) diff --git a/cedar-policy-cli/src/lib.rs b/cedar-policy-cli/src/lib.rs index 8ca55f4b38..cebe29decd 100644 --- a/cedar-policy-cli/src/lib.rs +++ b/cedar-policy-cli/src/lib.rs @@ -89,8 +89,6 @@ pub enum Commands { Evaluate(EvaluateArgs), /// Validate a policy set against a schema Validate(ValidateArgs), - /// Compute the entity manifest for a schema and policy set - EntityManifest(EntityManifestArgs), /// Check that policies successfully parse CheckParse(CheckParseArgs), /// Link a template @@ -156,12 +154,6 @@ pub enum SchemaFormat { Json, } -#[derive(Debug, Clone, Copy, ValueEnum)] -pub enum ManifestFormat { - /// JSON format- only one for now - Json, -} - impl Default for SchemaFormat { fn default() -> Self { Self::Cedar @@ -178,22 +170,6 @@ pub enum ValidationMode { Partial, } -#[derive(Args, Debug)] -pub struct EntityManifestArgs { - /// File containing the schema - #[arg(short, long = "schema", value_name = "FILE")] - pub schema_file: String, - /// Policies args (incorporated by reference) - #[command(flatten)] - pub policies: PoliciesArgs, - /// Schema format (Human-readable or JSON) - #[arg(long, value_enum, default_value_t = SchemaFormat::Human)] - pub schema_format: SchemaFormat, - #[arg(long, value_enum, default_value_t = ManifestFormat::Json)] - /// Manifest format (Human-readable or JSON) - pub manifest_format: ManifestFormat, -} - #[derive(Args, Debug)] pub struct ValidateArgs { /// File containing the schema @@ -721,39 +697,6 @@ pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode { } } -pub fn entity_manifest(args: &EntityManifestArgs) -> CedarExitCode { - let pset = match args.policies.get_policy_set() { - Ok(pset) => pset, - Err(e) => { - println!("{e:?}"); - return CedarExitCode::Failure; - } - }; - - let schema = match read_schema_file(&args.schema_file, args.schema_format) { - Ok(schema) => schema, - Err(e) => { - println!("{e:?}"); - return CedarExitCode::Failure; - } - }; - - let manifest = match compute_entity_manifest(&schema, &pset) { - Ok(manifest) => manifest, - Err(e) => { - println!("{e:?}"); - return CedarExitCode::Failure; - } - }; - match args.manifest_format { - ManifestFormat::Json => { - println!("{}", serde_json::to_string_pretty(&manifest).unwrap()); - } - } - - CedarExitCode::Success -} - pub fn validate(args: &ValidateArgs) -> CedarExitCode { let mode = match args.validation_mode { ValidationMode::Strict => cedar_policy::ValidationMode::Strict, diff --git a/cedar-policy-cli/src/main.rs b/cedar-policy-cli/src/main.rs index 3d8948e72f..3772e634d1 100644 --- a/cedar-policy-cli/src/main.rs +++ b/cedar-policy-cli/src/main.rs @@ -46,7 +46,6 @@ fn main() -> CedarExitCode { Commands::Evaluate(args) => evaluate(&args).0, Commands::CheckParse(args) => check_parse(&args), Commands::Validate(args) => validate(&args), - Commands::EntityManifest(args) => entity_manifest(&args), Commands::Format(args) => format_policies(&args), Commands::Link(args) => link(&args), Commands::TranslatePolicy(args) => translate_policy(&args), diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index 9b32b1ac6a..6cb8929999 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -286,7 +286,7 @@ impl AccessTrie { /// Computes an [`EntitySliceManifest`] from the schema and policies. /// The policies must validate against the schema in strict mode, /// otherwise an error is returned. -pub fn compute_entity_slice_manifest( +pub fn compute_entity_manifest( schema: &ValidatorSchema, policies: &PolicySet, ) -> Result { @@ -604,8 +604,7 @@ action Read appliesTo { .unwrap() .0; - let entity_manifest = - compute_entity_slice_manifest(&schema, &pset).expect("Should succeed"); + let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = r#" { "per_action": [ @@ -671,8 +670,7 @@ action Read appliesTo { .unwrap() .0; - let entity_manifest = - compute_entity_slice_manifest(&schema, &pset).expect("Should succeed"); + let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = r#" { "per_action": [ @@ -728,8 +726,7 @@ action Read appliesTo { .unwrap() .0; - let entity_manifest = - compute_entity_slice_manifest(&schema, &pset).expect("Should succeed"); + let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = r#" { "per_action": [ @@ -806,8 +803,7 @@ action Read appliesTo { .unwrap() .0; - let entity_manifest = - compute_entity_slice_manifest(&schema, &pset).expect("Should succeed"); + let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = r#" { "per_action": [ @@ -936,8 +932,7 @@ action Read appliesTo { .unwrap() .0; - let entity_manifest = - compute_entity_slice_manifest(&schema, &pset).expect("Should succeed"); + let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = r#" { "per_action": [ @@ -1033,8 +1028,7 @@ action BeSad appliesTo { .unwrap() .0; - let entity_manifest = - compute_entity_slice_manifest(&schema, &pset).expect("Should succeed"); + let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = r#" { "per_action": [ diff --git a/cedar-policy/CHANGELOG.md b/cedar-policy/CHANGELOG.md index f73a2a16ac..6816b757e7 100644 --- a/cedar-policy/CHANGELOG.md +++ b/cedar-policy/CHANGELOG.md @@ -13,7 +13,19 @@ Starting with version 3.2.4, changes marked with a star (*) are _language breaki Cedar Language Version: 4.0 ### Added - +- A new experimental API (`compute_entity_manifest`) + that provides the Entity Manifest: a data + structure that describes what data is required to satisfy a + Cedar request. Entity Manifests improve Cedar performance dramatically + in a safe way. +- JSON representation for Policy Sets, along with methods like + `::from_json_value/file/str` and `::to_json` for `PolicySet`. (#783, + resolving #549) +- Added methods for reading and writing individual `Entity`s as JSON + (resolving #807) +- `Context::into_iter` to get the contents of a `Context` and `Context::merge` + to combine `Context`s, returning an error on duplicate keys (#1027, + resolving #1013) - Additional functionality to the JSON FFI including parsing utilities (#1079) and conversion between the Cedar and JSON formats (#1087) - (*) Schema JSON syntax now accepts a type `EntityOrCommon` representing a diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 330c912b6b..2fee4440d0 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -23,7 +23,7 @@ )] mod id; -use cedar_policy_validator::entity_slicing::compute_entity_slice_manifest; +use cedar_policy_validator::entity_slicing; pub use cedar_policy_validator::entity_slicing::{ AccessTrie, EntityManifest, EntityRoot, EntitySliceError, Fields, RootAccessTrie, }; @@ -4286,5 +4286,5 @@ pub fn compute_entity_manifest( schema: &Schema, pset: &PolicySet, ) -> Result { - compute_entity_slice_manifest(&schema.0, &pset.ast) + entity_slicing::compute_entity_manifest(&schema.0, &pset.ast) } From 564dd1e7378c5925b64ef66c543eab6759f873e5 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 30 Jul 2024 12:40:38 -0700 Subject: [PATCH 17/56] feature flag for entity manifests Signed-off-by: oflatt --- cedar-policy/Cargo.toml | 3 ++- cedar-policy/src/api.rs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cedar-policy/Cargo.toml b/cedar-policy/Cargo.toml index 94a182c777..9d91bde598 100644 --- a/cedar-policy/Cargo.toml +++ b/cedar-policy/Cargo.toml @@ -46,7 +46,8 @@ corpus-timing = [] # Experimental features. # Enable all experimental features with `cargo build --features "experimental"` -experimental = ["partial-eval", "permissive-validate", "partial-validate"] +experimental = ["partial-eval", "permissive-validate", "partial-validate", "entity-manifest"] +entity-manifest = [] partial-eval = ["cedar-policy-core/partial-eval", "cedar-policy-validator/partial-eval"] permissive-validate = [] partial-validate = ["cedar-policy-validator/partial-validate"] diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 2fee4440d0..5317128158 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -24,6 +24,7 @@ mod id; use cedar_policy_validator::entity_slicing; +#[cfg(feature = "entity-manifest")] pub use cedar_policy_validator::entity_slicing::{ AccessTrie, EntityManifest, EntityRoot, EntitySliceError, Fields, RootAccessTrie, }; @@ -4282,6 +4283,7 @@ action CreateList in Create appliesTo { /// The manifest describes the data required to answer requests /// for each action type. #[doc = include_str!("../experimental_warning.md")] +#[cfg(feature = "entity-manifest")] pub fn compute_entity_manifest( schema: &Schema, pset: &PolicySet, From 1118d960b865b76672fd40a2816d43a0f02895e8 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 30 Jul 2024 12:44:09 -0700 Subject: [PATCH 18/56] panic safety comments Signed-off-by: oflatt --- cedar-policy-validator/src/entity_slicing.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index 6cb8929999..6ef1b7c80b 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -314,7 +314,7 @@ pub fn compute_entity_manifest( // Also, should we run full validation instead of just // typechecking? Validation does a little more right? PolicyCheck::Fail(errors) => { - // PANIC SAFETY policy check fail + // PANIC SAFETY: policy check fail // should be a non-empty vector. #[allow(clippy::expect_used)] Err(errors @@ -455,6 +455,8 @@ fn add_to_root_trie( ExprKind::GetAttr { .. } => { let flat_slice = get_expr_path(expr, policy_id)?; + // PANIC SAFETY: Successfuly typechecked expressions + // should always have annotated types. #[allow(clippy::expect_used)] let leaf_field = if should_load_all { type_to_access_trie( From 906757d7498f55cadb11ec64f33e6d9bd9565772 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 30 Jul 2024 12:58:13 -0700 Subject: [PATCH 19/56] remove line breaks from panic safety comments Signed-off-by: oflatt --- cedar-policy-validator/src/entity_slicing.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index 6ef1b7c80b..dfbb9a4c6b 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -314,8 +314,7 @@ pub fn compute_entity_manifest( // Also, should we run full validation instead of just // typechecking? Validation does a little more right? PolicyCheck::Fail(errors) => { - // PANIC SAFETY: policy check fail - // should be a non-empty vector. + // PANIC SAFETY: policy check fail should be a non-empty vector. #[allow(clippy::expect_used)] Err(errors .first() @@ -455,8 +454,7 @@ fn add_to_root_trie( ExprKind::GetAttr { .. } => { let flat_slice = get_expr_path(expr, policy_id)?; - // PANIC SAFETY: Successfuly typechecked expressions - // should always have annotated types. + // PANIC SAFETY: Successfuly typechecked expressions should always have annotated types. #[allow(clippy::expect_used)] let leaf_field = if should_load_all { type_to_access_trie( From 18ec86eecb49e878f90cf4fda44863a68667aa4d Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 30 Jul 2024 13:04:33 -0700 Subject: [PATCH 20/56] fix doc comments Signed-off-by: oflatt --- cedar-policy-core/src/ast/request.rs | 1 - cedar-policy-validator/src/entity_slicing.rs | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cedar-policy-core/src/ast/request.rs b/cedar-policy-core/src/ast/request.rs index 87afdb47f6..5d46b1c667 100644 --- a/cedar-policy-core/src/ast/request.rs +++ b/cedar-policy-core/src/ast/request.rs @@ -50,7 +50,6 @@ pub struct Request { } /// Represents the principal, action, and resource types. -/// Used to index the [`EntityManifest`] #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] pub struct RequestType { /// Principal type diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs index dfbb9a4c6b..9fef092b96 100644 --- a/cedar-policy-validator/src/entity_slicing.rs +++ b/cedar-policy-validator/src/entity_slicing.rs @@ -117,6 +117,7 @@ struct AccessPath { /// Entity manifest computation does not handle the full /// cedar language. In particular, the policies must follow the /// following grammar: +/// ```text /// = /// in /// + @@ -127,7 +128,7 @@ struct AccessPath { /// has /// /// -/// +/// ``` /// The `get_expr_path` function handles `datapath-expr` expressions. /// This error message tells the user not to use certain operators /// before accessing record or entity attributes, breaking this grammar. @@ -230,7 +231,7 @@ impl AccessPath { } impl RootAccessTrie { - /// Create an empty [`PrimarySlice`] that requests nothing. + /// Create an empty [`RootAccessTrie`] that requests nothing. pub fn new() -> Self { Self { trie: Default::default(), @@ -283,7 +284,7 @@ impl AccessTrie { } } -/// Computes an [`EntitySliceManifest`] from the schema and policies. +/// Computes an [`EntityManifest`] from the schema and policies. /// The policies must validate against the schema in strict mode, /// otherwise an error is returned. pub fn compute_entity_manifest( From bce7c85a2607383cf8927633b647b9e5db802db1 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 30 Jul 2024 14:03:20 -0700 Subject: [PATCH 21/56] fix up build with unused import Signed-off-by: oflatt --- cedar-policy/src/api.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 5317128158..b4a6f317d8 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -23,6 +23,7 @@ )] mod id; +#[cfg(feature = "entity-manifest")] use cedar_policy_validator::entity_slicing; #[cfg(feature = "entity-manifest")] pub use cedar_policy_validator::entity_slicing::{ From 66e5a52ec9f18d5e4bea90039b15ea5d0a22467a Mon Sep 17 00:00:00 2001 From: oflatt Date: Wed, 31 Jul 2024 11:42:43 -0700 Subject: [PATCH 22/56] re-name to entity manifest file Signed-off-by: oflatt --- cedar-policy-validator/src/entity_slicing.rs | 1087 ------------------ cedar-policy-validator/src/lib.rs | 2 +- cedar-policy/src/api.rs | 6 +- 3 files changed, 4 insertions(+), 1091 deletions(-) delete mode 100644 cedar-policy-validator/src/entity_slicing.rs diff --git a/cedar-policy-validator/src/entity_slicing.rs b/cedar-policy-validator/src/entity_slicing.rs deleted file mode 100644 index 9fef092b96..0000000000 --- a/cedar-policy-validator/src/entity_slicing.rs +++ /dev/null @@ -1,1087 +0,0 @@ -//! Entity Slicing - -use std::collections::HashMap; -use std::fmt::{Display, Formatter}; - -use cedar_policy_core::ast::{ - BinaryOp, EntityUID, Expr, ExprKind, Literal, PolicyID, PolicySet, RequestType, UnaryOp, Var, -}; -use cedar_policy_core::entities::err::EntitiesError; -use cedar_policy_core::impl_diagnostic_from_source_loc_field; -use cedar_policy_core::parser::Loc; -use miette::Diagnostic; -use serde::{Deserialize, Serialize}; -use serde_with::serde_as; -use smol_str::SmolStr; -use thiserror::Error; - -use crate::ValidationError; -use crate::{ - typecheck::{PolicyCheck, Typechecker}, - types::{EntityRecordKind, Type}, - ValidationMode, ValidatorSchema, -}; - -/// Data structure storing what data is needed -/// based on the the [`RequestType`]. -/// For each request type, the [`EntityManifest`] stores -/// a [`RootAccessTrie`] of data to retrieve. -#[doc = include_str!("../../cedar-policy/experimental_warning.md")] -#[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct EntityManifest -where - T: Clone, -{ - /// A map from request types to [`RootAccessTrie`]s. - #[serde_as(as = "Vec<(_, _)>")] - #[serde(bound(deserialize = "T: Default"))] - pub per_action: HashMap>, -} - -/// A map of data fields to [`AccessTrie`]s. -/// The keys to this map form the edges in the access trie, -/// pointing to sub-tries. -#[doc = include_str!("../../cedar-policy/experimental_warning.md")] -pub type Fields = HashMap>>; - -/// The root of an entity slice. -#[doc = include_str!("../../cedar-policy/experimental_warning.md")] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] -pub enum EntityRoot { - /// Literal entity ids - Literal(EntityUID), - /// A Cedar variable - 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}"), - } - } -} - -/// A [`RootAccessTrie`] is a trie describing -/// data paths to retrieve. Each edge in the trie -/// is either a record or entity dereference. -/// -/// If an entity or record field does not exist in the backing store, -/// it is safe to stop loading data at that point. -#[doc = include_str!("../../cedar-policy/experimental_warning.md")] -#[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RootAccessTrie -where - T: Clone, -{ - #[serde_as(as = "Vec<(_, _)>")] - #[serde(bound(deserialize = "T: Default"))] - /// The data that needs to be loaded, organized by root. - pub trie: HashMap>, -} - -/// An entity slice- tells users a tree of data to load -#[doc = include_str!("../../cedar-policy/experimental_warning.md")] -#[serde_as] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -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<(_, _)>")] - pub children: Fields, - /// For entity types, this boolean may be `true` - /// to signal that all the parents in the entity hierarchy - /// are required (transitively). - pub parents_required: bool, - /// Optional data annotation, usually used for type information. - #[serde(skip_serializing, skip_deserializing)] - #[serde(bound(deserialize = "T: Default"))] - pub data: T, -} - -/// A data path that may end with requesting the parents of -/// an entity. -#[derive(Debug, Clone, PartialEq, Eq)] -struct AccessPath { - /// The root variable that begins the data path - pub root: EntityRoot, - /// The path of fields of entities or structs - pub path: Vec, - /// Request all the parents in the entity hierarchy of this entity. - pub parents_required: bool, -} - -/// Entity manifest computation does not handle the full -/// cedar language. In particular, the policies must follow the -/// following grammar: -/// ```text -/// = -/// in -/// + -/// if { } { } -/// ... all other cedar operators not mentioned by datapath-expr - -/// = . -/// has -/// -/// -/// ``` -/// The `get_expr_path` function handles `datapath-expr` expressions. -/// This error message tells the user not to use certain operators -/// before accessing record or entity attributes, breaking this grammar. -#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)] -#[error("For policy `{policy_id}`, failed to analyze expression while computing entity manifest.`")] -pub struct FailedAnalysisError { - /// Source location - pub source_loc: Option, - /// Policy ID where the error occurred - pub policy_id: PolicyID, - /// The kind of the expression that was unexpected - pub expr_kind: ExprKind>, -} - -impl Diagnostic for FailedAnalysisError { - impl_diagnostic_from_source_loc_field!(); - - fn help<'a>(&'a self) -> Option> { - Some(Box::new(format!( - "Entity slicing failed to analyze expression: {} operators are not allowed before accessing record or entity attributes.", - self.expr_kind.operator_description() - ))) - } -} - -/// An error generated by entity slicing. -/// See [`FailedAnalysisError`] for details on the fragment -/// of Cedar handled by entity slicing. -#[derive(Debug, Error, Diagnostic)] -#[non_exhaustive] -pub enum EntitySliceError { - /// A validation error was encountered - #[error(transparent)] - #[diagnostic(transparent)] - ValidationError(#[from] ValidationError), - /// A entities error was encountered - #[error(transparent)] - #[diagnostic(transparent)] - EntitiesError(#[from] EntitiesError), - - /// The request was partial - #[error("Entity slicing requires a fully concrete request. Got a partial request.")] - PartialRequestError, - /// A policy was partial - #[error( - "Entity slicing requires fully concrete policies. Got a policy with an unknown expression." - )] - PartialExpressionError, - - /// A policy was not analyzable because it used unsupported operators - /// before a [`ExprKind::GetAttr`] - /// See [`FailedAnalysisError`] for more details. - #[error(transparent)] - #[diagnostic(transparent)] - FailedAnalysis(#[from] FailedAnalysisError), -} - -/// Union two tries by combining the fields. -fn union_fields(first: &Fields, second: &Fields) -> Fields { - let mut res = first.clone(); - for (key, value) in second { - if let Some(existing) = res.get(key) { - res.insert(key.clone(), Box::new((*existing).union(value))); - } else { - res.insert(key.clone(), value.clone()); - } - } - res -} - -impl AccessPath { - /// Convert a [`AccessPath`] into corresponding [`RootAccessTrie`]. - fn to_primary_slice(&self) -> RootAccessTrie { - self.to_primary_slice_with_leaf(AccessTrie { - parents_required: true, - children: Default::default(), - data: (), - }) - } - - /// Convert an [`AccessPath`] to a [`RootAccessTrie`], and also - /// add a full trie as the leaf at the end. - fn to_primary_slice_with_leaf(&self, leaf_trie: AccessTrie) -> RootAccessTrie { - let mut current = leaf_trie; - // reverse the path, visiting the last access first - for field in self.path.iter().rev() { - let mut fields = HashMap::new(); - fields.insert(field.clone(), Box::new(current)); - current = AccessTrie { - parents_required: false, - children: fields, - data: (), - }; - } - - let mut primary_map = HashMap::new(); - primary_map.insert(self.root.clone(), current); - RootAccessTrie { trie: primary_map } - } -} - -impl RootAccessTrie { - /// Create an empty [`RootAccessTrie`] that requests nothing. - pub fn new() -> Self { - Self { - trie: Default::default(), - } - } -} - -impl RootAccessTrie { - /// Union two [`RootAccessTrie`]s together. - /// The new trie requests the data from both of the original. - fn union(&self, other: &Self) -> Self { - let mut res = self.clone(); - for (key, value) in &other.trie { - if let Some(existing) = res.trie.get(key) { - res.trie.insert(key.clone(), (*existing).union(value)); - } else { - res.trie.insert(key.clone(), value.clone()); - } - } - res - } -} - -impl Default for RootAccessTrie { - fn default() -> Self { - Self::new() - } -} - -impl AccessTrie { - /// Union two [`AccessTrie`]s together. - /// The new trie requests the data from both of the original. - fn union(&self, other: &Self) -> Self { - Self { - children: union_fields(&self.children, &other.children), - parents_required: self.parents_required || other.parents_required, - data: self.data.clone(), - } - } -} - -impl AccessTrie { - /// A new trie that requests no data. - fn new() -> Self { - Self { - children: Default::default(), - parents_required: false, - data: (), - } - } -} - -/// Computes an [`EntityManifest`] from the schema and policies. -/// The policies must validate against the schema in strict mode, -/// otherwise an error is returned. -pub fn compute_entity_manifest( - schema: &ValidatorSchema, - policies: &PolicySet, -) -> Result { - let mut manifest: HashMap = HashMap::new(); - - // now, for each policy we add the data it requires to the manifest - for policy in policies.policies() { - // typecheck the policy and get all the request environments - let typechecker = Typechecker::new(schema, ValidationMode::Strict, policy.id().clone()); - let request_envs = typechecker.typecheck_by_request_env(policy.template()); - for (request_env, policy_check) in request_envs { - // match on the typechecking answer - let new_primary_slice = match policy_check { - PolicyCheck::Success(typechecked_expr) => { - // compute the trie from the typechecked expr - // using static analysis - compute_root_trie(&typechecked_expr, policy.id()) - } - PolicyCheck::Irrelevant(_) => { - // this policy is ireelevant, so we need no data - Ok(RootAccessTrie::new()) - } - - // TODO is returning the first error correct? - // Also, should we run full validation instead of just - // typechecking? Validation does a little more right? - PolicyCheck::Fail(errors) => { - // PANIC SAFETY: policy check fail should be a non-empty vector. - #[allow(clippy::expect_used)] - Err(errors - .first() - .expect("Policy check failed without an error") - .clone() - .into()) - } - }?; - - let request_type = request_env - .to_request_type() - .ok_or(EntitySliceError::PartialRequestError)?; - // Add to the manifest based on the request type. - if let Some(existing) = manifest.get_mut(&request_type) { - *existing = existing.union(&new_primary_slice); - } else { - manifest.insert(request_type, new_primary_slice); - } - } - } - - Ok(EntityManifest { - per_action: manifest, - }) -} - -/// A static analysis on type-annotated cedar expressions. -/// Computes the [`RootAccessTrie`] representing all the data required -/// to evaluate the expression. -fn compute_root_trie( - expr: &Expr>, - policy_id: &PolicyID, -) -> Result { - let mut primary_slice = RootAccessTrie::new(); - add_to_root_trie(&mut primary_slice, expr, policy_id, false)?; - Ok(primary_slice) -} - -/// Add the expression's requested data to the [`RootAccessTrie`]. -/// This handles s from the grammar (see [`FailedAnalysisError`]) -/// while [`get_expr_path`] handles the s. -fn add_to_root_trie( - root_trie: &mut RootAccessTrie, - expr: &Expr>, - policy_id: &PolicyID, - should_load_all: bool, -) -> Result<(), EntitySliceError> { - match expr.expr_kind() { - // Literals, variables, and unkonwns without any GetAttr operations - // on them are okay, since no fields need to be loaded. - ExprKind::Lit(_) => (), - ExprKind::Var(_) => (), - ExprKind::Slot(_) => (), - ExprKind::Unknown(_) => return Err(EntitySliceError::PartialExpressionError), - ExprKind::If { - test_expr, - then_expr, - else_expr, - } => { - add_to_root_trie(root_trie, test_expr, policy_id, should_load_all)?; - add_to_root_trie(root_trie, then_expr, policy_id, should_load_all)?; - add_to_root_trie(root_trie, else_expr, policy_id, should_load_all)?; - } - ExprKind::And { left, right } => { - add_to_root_trie(root_trie, left, policy_id, should_load_all)?; - add_to_root_trie(root_trie, right, policy_id, should_load_all)?; - } - ExprKind::Or { left, right } => { - add_to_root_trie(root_trie, left, policy_id, should_load_all)?; - add_to_root_trie(root_trie, right, policy_id, should_load_all)?; - } - ExprKind::UnaryApp { op, arg } => match op { - UnaryOp::Not => add_to_root_trie(root_trie, arg, policy_id, should_load_all)?, - UnaryOp::Neg => add_to_root_trie(root_trie, arg, policy_id, should_load_all)?, - }, - ExprKind::BinaryApp { op, arg1, arg2 } => match op { - // Special case! Equality between records requires - // that we load all fields. - // This could be made more precise if we check the type. - BinaryOp::Eq => { - add_to_root_trie(root_trie, arg1, policy_id, true)?; - add_to_root_trie(root_trie, arg1, policy_id, true)?; - } - BinaryOp::In => { - // Recur normally on the rhs - add_to_root_trie(root_trie, arg2, policy_id, should_load_all)?; - - // The lhs should be a datapath expression. - let mut flat_slice = get_expr_path(arg1, policy_id)?; - flat_slice.parents_required = true; - *root_trie = root_trie.union(&flat_slice.to_primary_slice()); - } - BinaryOp::Contains | BinaryOp::ContainsAll | BinaryOp::ContainsAny => { - // Like equality, another special case for records. - add_to_root_trie(root_trie, arg1, policy_id, true)?; - add_to_root_trie(root_trie, arg2, policy_id, true)?; - } - BinaryOp::Less | BinaryOp::LessEq | BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul => { - // These operators work on values, so no special - // case is needed. - add_to_root_trie(root_trie, arg1, policy_id, should_load_all)?; - add_to_root_trie(root_trie, arg2, policy_id, should_load_all)?; - } - }, - ExprKind::ExtensionFunctionApp { fn_name: _, args } => { - // WARNING: this code assumes that extension functions - // don't take full structs as inputs. - // If they did, we would need to use logic similar to the Eq binary operator. - for arg in args.iter() { - add_to_root_trie(root_trie, arg, policy_id, should_load_all)?; - } - } - ExprKind::Like { expr, pattern: _ } => { - add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; - } - ExprKind::Is { - expr, - entity_type: _, - } => { - add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; - } - ExprKind::Set(contents) => { - for expr in &**contents { - add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; - } - } - ExprKind::Record(content) => { - for expr in content.values() { - add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; - } - } - ExprKind::HasAttr { expr, attr } => { - let mut flat_slice = get_expr_path(expr, policy_id)?; - flat_slice.path.push(attr.clone()); - *root_trie = root_trie.union(&flat_slice.to_primary_slice()); - } - ExprKind::GetAttr { .. } => { - let flat_slice = get_expr_path(expr, policy_id)?; - - // PANIC SAFETY: Successfuly typechecked expressions should always have annotated types. - #[allow(clippy::expect_used)] - let leaf_field = if should_load_all { - type_to_access_trie( - expr.data() - .as_ref() - .expect("Typechecked expression missing type"), - ) - } else { - AccessTrie::new() - }; - - *root_trie = flat_slice.to_primary_slice_with_leaf(leaf_field); - } - }; - - Ok(()) -} - -/// Compute the full [`AccessTrie`] required for the type. -fn type_to_access_trie(ty: &Type) -> AccessTrie { - match ty { - // if it's not an entity or record, slice ends here - Type::ExtensionType { .. } - | Type::Never - | Type::True - | Type::False - | Type::Primitive { .. } - | Type::Set { .. } => AccessTrie::new(), - Type::EntityOrRecord(record_type) => entity_or_record_to_access_trie(record_type), - } -} - -/// Compute the full [`AccessTrie`] for the given entity or record type. -fn entity_or_record_to_access_trie(ty: &EntityRecordKind) -> AccessTrie { - match ty { - EntityRecordKind::ActionEntity { name: _, attrs } - | EntityRecordKind::Record { - attrs, - open_attributes: _, - } => { - let mut fields = HashMap::new(); - for (attr_name, attr_type) in attrs.iter() { - fields.insert( - attr_name.clone(), - Box::new(type_to_access_trie(&attr_type.attr_type)), - ); - } - AccessTrie { - children: fields, - parents_required: false, - data: (), - } - } - - EntityRecordKind::Entity(_) | EntityRecordKind::AnyEntity => { - // no need to load data for entities, which are compared - // using ids - AccessTrie::new() - } - } -} - -/// Given an expression, get the corresponding data path -/// starting with a variable. -/// If the expression is not a ``, return an error. -/// See [`FailedAnalysisError`] for more information. -fn get_expr_path( - expr: &Expr>, - policy_id: &PolicyID, -) -> Result { - Ok(match expr.expr_kind() { - ExprKind::Slot(slot_id) => { - if slot_id.is_principal() { - AccessPath { - root: EntityRoot::Var(Var::Principal), - path: vec![], - parents_required: false, - } - } else { - assert!(slot_id.is_resource()); - AccessPath { - root: EntityRoot::Var(Var::Resource), - path: vec![], - parents_required: false, - } - } - } - ExprKind::Var(var) => AccessPath { - root: EntityRoot::Var(*var), - path: vec![], - parents_required: false, - }, - ExprKind::GetAttr { expr, attr } => { - let mut slice = get_expr_path(expr, policy_id)?; - slice.path.push(attr.clone()); - slice - } - ExprKind::Lit(Literal::EntityUID(literal)) => AccessPath { - root: EntityRoot::Literal((**literal).clone()), - path: vec![], - parents_required: false, - }, - ExprKind::Unknown(_) => Err(EntitySliceError::PartialExpressionError)?, - // all other variants of expressions result in failure to analyze. - _ => Err(EntitySliceError::FailedAnalysis(FailedAnalysisError { - source_loc: expr.source_loc().cloned(), - policy_id: policy_id.clone(), - expr_kind: expr.expr_kind().clone(), - }))?, - }) -} - -#[cfg(test)] -mod entity_slice_tests { - use cedar_policy_core::{ast::PolicyID, extensions::Extensions, parser::parse_policy}; - - use super::*; - - #[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 = ValidatorSchema::from_str_natural( - " -entity User = { - name: String, -}; - -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 expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "Read" - }, - "resource": "Document" - }, - { - "trie": [ - [ - { - "Var": "principal" - }, - { - "children": [ - [ - "name", - { - "children": [], - "parents_required": false - } - ] - ], - "parents_required": false - } - ] - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).unwrap(); - assert_eq!(entity_manifest, expected_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 = ValidatorSchema::from_str_natural( - " -entity User = { - name: String, -}; - -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 expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "Read" - }, - "resource": "Document" - }, - { - "trie": [ - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).unwrap(); - assert_eq!(entity_manifest, expected_manifest); - } - - #[test] - fn test_entity_manifest_parents_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_str_natural( - " -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 expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "Read" - }, - "resource": "Document" - }, - { - "trie": [ - [ - { - "Var": "principal" - }, - { - "children": [ - [ - "manager", - { - "children": [], - "parents_required": true - } - ] - ], - "parents_required": true - } - ] - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).unwrap(); - assert_eq!(entity_manifest, expected_manifest); - } - - #[test] - fn test_entity_manifest_multiple_types() { - 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 = ValidatorSchema::from_str_natural( - " -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 entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); - let expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "Read" - }, - "resource": "Document" - }, - { - "trie": [ - [ - { - "Var": "principal" - }, - { - "children": [ - [ - "name", - { - "children": [], - "parents_required": false - } - ] - ], - "parents_required": false - } - ] - ] - } - ], - [ - { - "principal": "OtherUserType", - "action": { - "ty": "Action", - "eid": "Read" - }, - "resource": "Document" - }, - { - "trie": [ - [ - { - "Var": "principal" - }, - { - "children": [ - [ - "name", - { - "children": [], - "parents_required": false - } - ] - ], - "parents_required": false - } - ] - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).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_str_natural( - " -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 expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "Read" - }, - "resource": "Document" - }, - { - "trie": [ - [ - { - "Var": "resource" - }, - { - "children": [ - [ - "metadata", - { - "children": [ - [ - "owner", - { - "children": [], - "parents_required": false - } - ] - ], - "parents_required": false - } - ], - [ - "readers", - { - "children": [], - "parents_required": false - } - ] - ], - "parents_required": false - } - ] - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).unwrap(); - assert_eq!(entity_manifest, expected_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_str_natural( - " -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"); - let expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "BeSad" - }, - "resource": "Document" - }, - { - "trie": [ - [ - { - "Var": "principal" - }, - { - "children": [ - [ - "metadata", - { - "children": [ - [ - "nickname", - { - "children": [], - "parents_required": false - } - ], - [ - "friends", - { - "children": [], - "parents_required": false - } - ] - ], - "parents_required": false - } - ] - ], - "parents_required": false - } - ] - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).unwrap(); - assert_eq!(entity_manifest, expected_manifest); - } -} diff --git a/cedar-policy-validator/src/lib.rs b/cedar-policy-validator/src/lib.rs index 9a8995f89c..97c0bcdf42 100644 --- a/cedar-policy-validator/src/lib.rs +++ b/cedar-policy-validator/src/lib.rs @@ -35,7 +35,7 @@ use cedar_policy_core::ast::{Policy, PolicySet, Template}; use serde::Serialize; use std::collections::HashSet; -pub mod entity_slicing; +pub mod entity_manifest; mod err; pub use err::*; mod coreschema; diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index b4a6f317d8..c47e3cb2e0 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -24,9 +24,9 @@ mod id; #[cfg(feature = "entity-manifest")] -use cedar_policy_validator::entity_slicing; +use cedar_policy_validator::entity_manifest; #[cfg(feature = "entity-manifest")] -pub use cedar_policy_validator::entity_slicing::{ +pub use cedar_policy_validator::entity_manifest::{ AccessTrie, EntityManifest, EntityRoot, EntitySliceError, Fields, RootAccessTrie, }; use cedar_policy_validator::typecheck::{PolicyCheck, Typechecker}; @@ -4289,5 +4289,5 @@ pub fn compute_entity_manifest( schema: &Schema, pset: &PolicySet, ) -> Result { - entity_slicing::compute_entity_manifest(&schema.0, &pset.ast) + entity_manifest::compute_entity_manifest(&schema.0, &pset.ast) } From cd9f2e9c9216840c2d38c08278e3a18cd3da2af6 Mon Sep 17 00:00:00 2001 From: oflatt Date: Wed, 31 Jul 2024 11:46:02 -0700 Subject: [PATCH 23/56] oops, add entity manifest file Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 1103 +++++++++++++++++ 1 file changed, 1103 insertions(+) create mode 100644 cedar-policy-validator/src/entity_manifest.rs diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs new file mode 100644 index 0000000000..3e9919317e --- /dev/null +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -0,0 +1,1103 @@ +//! Entity Manifest definition and static analysis. + +/* + * 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. + */ + +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; + +use cedar_policy_core::ast::{ + BinaryOp, EntityUID, Expr, ExprKind, Literal, PolicyID, PolicySet, RequestType, UnaryOp, Var, +}; +use cedar_policy_core::entities::err::EntitiesError; +use cedar_policy_core::impl_diagnostic_from_source_loc_field; +use cedar_policy_core::parser::Loc; +use miette::Diagnostic; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use smol_str::SmolStr; +use thiserror::Error; + +use crate::ValidationError; +use crate::{ + typecheck::{PolicyCheck, Typechecker}, + types::{EntityRecordKind, Type}, + ValidationMode, ValidatorSchema, +}; + +/// Data structure storing what data is needed +/// based on the the [`RequestType`]. +/// For each request type, the [`EntityManifest`] stores +/// a [`RootAccessTrie`] of data to retrieve. +#[doc = include_str!("../../cedar-policy/experimental_warning.md")] +#[serde_as] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EntityManifest +where + T: Clone, +{ + /// A map from request types to [`RootAccessTrie`]s. + #[serde_as(as = "Vec<(_, _)>")] + #[serde(bound(deserialize = "T: Default"))] + pub per_action: HashMap>, +} + +/// A map of data fields to [`AccessTrie`]s. +/// The keys to this map form the edges in the access trie, +/// pointing to sub-tries. +#[doc = include_str!("../../cedar-policy/experimental_warning.md")] +pub type Fields = HashMap>>; + +/// The root of an entity slice. +#[doc = include_str!("../../cedar-policy/experimental_warning.md")] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub enum EntityRoot { + /// Literal entity ids + Literal(EntityUID), + /// A Cedar variable + 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}"), + } + } +} + +/// A [`RootAccessTrie`] is a trie describing +/// data paths to retrieve. Each edge in the trie +/// is either a record or entity dereference. +/// +/// If an entity or record field does not exist in the backing store, +/// it is safe to stop loading data at that point. +#[doc = include_str!("../../cedar-policy/experimental_warning.md")] +#[serde_as] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RootAccessTrie +where + T: Clone, +{ + #[serde_as(as = "Vec<(_, _)>")] + #[serde(bound(deserialize = "T: Default"))] + /// The data that needs to be loaded, organized by root. + pub trie: HashMap>, +} + +/// An entity slice- tells users a tree of data to load +#[doc = include_str!("../../cedar-policy/experimental_warning.md")] +#[serde_as] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +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<(_, _)>")] + pub children: Fields, + /// For entity types, this boolean may be `true` + /// to signal that all the parents in the entity hierarchy + /// are required (transitively). + pub parents_required: bool, + /// Optional data annotation, usually used for type information. + #[serde(skip_serializing, skip_deserializing)] + #[serde(bound(deserialize = "T: Default"))] + pub data: T, +} + +/// A data path that may end with requesting the parents of +/// an entity. +#[derive(Debug, Clone, PartialEq, Eq)] +struct AccessPath { + /// The root variable that begins the data path + pub root: EntityRoot, + /// The path of fields of entities or structs + pub path: Vec, + /// Request all the parents in the entity hierarchy of this entity. + pub parents_required: bool, +} + +/// Entity manifest computation does not handle the full +/// cedar language. In particular, the policies must follow the +/// following grammar: +/// ```text +/// = +/// in +/// + +/// if { } { } +/// ... all other cedar operators not mentioned by datapath-expr + +/// = . +/// has +/// +/// +/// ``` +/// The `get_expr_path` function handles `datapath-expr` expressions. +/// This error message tells the user not to use certain operators +/// before accessing record or entity attributes, breaking this grammar. +#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)] +#[error("For policy `{policy_id}`, failed to analyze expression while computing entity manifest.`")] +pub struct FailedAnalysisError { + /// Source location + pub source_loc: Option, + /// Policy ID where the error occurred + pub policy_id: PolicyID, + /// The kind of the expression that was unexpected + pub expr_kind: ExprKind>, +} + +impl Diagnostic for FailedAnalysisError { + impl_diagnostic_from_source_loc_field!(); + + fn help<'a>(&'a self) -> Option> { + Some(Box::new(format!( + "Entity slicing failed to analyze expression: {} operators are not allowed before accessing record or entity attributes.", + self.expr_kind.operator_description() + ))) + } +} + +/// An error generated by entity slicing. +/// See [`FailedAnalysisError`] for details on the fragment +/// of Cedar handled by entity slicing. +#[derive(Debug, Error, Diagnostic)] +#[non_exhaustive] +pub enum EntitySliceError { + /// A validation error was encountered + #[error(transparent)] + #[diagnostic(transparent)] + ValidationError(#[from] ValidationError), + /// A entities error was encountered + #[error(transparent)] + #[diagnostic(transparent)] + EntitiesError(#[from] EntitiesError), + + /// The request was partial + #[error("Entity slicing requires a fully concrete request. Got a partial request.")] + PartialRequestError, + /// A policy was partial + #[error( + "Entity slicing requires fully concrete policies. Got a policy with an unknown expression." + )] + PartialExpressionError, + + /// A policy was not analyzable because it used unsupported operators + /// before a [`ExprKind::GetAttr`] + /// See [`FailedAnalysisError`] for more details. + #[error(transparent)] + #[diagnostic(transparent)] + FailedAnalysis(#[from] FailedAnalysisError), +} + +/// Union two tries by combining the fields. +fn union_fields(first: &Fields, second: &Fields) -> Fields { + let mut res = first.clone(); + for (key, value) in second { + if let Some(existing) = res.get(key) { + res.insert(key.clone(), Box::new((*existing).union(value))); + } else { + res.insert(key.clone(), value.clone()); + } + } + res +} + +impl AccessPath { + /// Convert a [`AccessPath`] into corresponding [`RootAccessTrie`]. + fn to_primary_slice(&self) -> RootAccessTrie { + self.to_primary_slice_with_leaf(AccessTrie { + parents_required: true, + children: Default::default(), + data: (), + }) + } + + /// Convert an [`AccessPath`] to a [`RootAccessTrie`], and also + /// add a full trie as the leaf at the end. + fn to_primary_slice_with_leaf(&self, leaf_trie: AccessTrie) -> RootAccessTrie { + let mut current = leaf_trie; + // reverse the path, visiting the last access first + for field in self.path.iter().rev() { + let mut fields = HashMap::new(); + fields.insert(field.clone(), Box::new(current)); + current = AccessTrie { + parents_required: false, + children: fields, + data: (), + }; + } + + let mut primary_map = HashMap::new(); + primary_map.insert(self.root.clone(), current); + RootAccessTrie { trie: primary_map } + } +} + +impl RootAccessTrie { + /// Create an empty [`RootAccessTrie`] that requests nothing. + pub fn new() -> Self { + Self { + trie: Default::default(), + } + } +} + +impl RootAccessTrie { + /// Union two [`RootAccessTrie`]s together. + /// The new trie requests the data from both of the original. + fn union(&self, other: &Self) -> Self { + let mut res = self.clone(); + for (key, value) in &other.trie { + if let Some(existing) = res.trie.get(key) { + res.trie.insert(key.clone(), (*existing).union(value)); + } else { + res.trie.insert(key.clone(), value.clone()); + } + } + res + } +} + +impl Default for RootAccessTrie { + fn default() -> Self { + Self::new() + } +} + +impl AccessTrie { + /// Union two [`AccessTrie`]s together. + /// The new trie requests the data from both of the original. + fn union(&self, other: &Self) -> Self { + Self { + children: union_fields(&self.children, &other.children), + parents_required: self.parents_required || other.parents_required, + data: self.data.clone(), + } + } +} + +impl AccessTrie { + /// A new trie that requests no data. + fn new() -> Self { + Self { + children: Default::default(), + parents_required: false, + data: (), + } + } +} + +/// Computes an [`EntityManifest`] from the schema and policies. +/// The policies must validate against the schema in strict mode, +/// otherwise an error is returned. +pub fn compute_entity_manifest( + schema: &ValidatorSchema, + policies: &PolicySet, +) -> Result { + let mut manifest: HashMap = HashMap::new(); + + // now, for each policy we add the data it requires to the manifest + for policy in policies.policies() { + // typecheck the policy and get all the request environments + let typechecker = Typechecker::new(schema, ValidationMode::Strict, policy.id().clone()); + let request_envs = typechecker.typecheck_by_request_env(policy.template()); + for (request_env, policy_check) in request_envs { + // match on the typechecking answer + let new_primary_slice = match policy_check { + PolicyCheck::Success(typechecked_expr) => { + // compute the trie from the typechecked expr + // using static analysis + compute_root_trie(&typechecked_expr, policy.id()) + } + PolicyCheck::Irrelevant(_) => { + // this policy is ireelevant, so we need no data + Ok(RootAccessTrie::new()) + } + + // TODO is returning the first error correct? + // Also, should we run full validation instead of just + // typechecking? Validation does a little more right? + PolicyCheck::Fail(errors) => { + // PANIC SAFETY: policy check fail should be a non-empty vector. + #[allow(clippy::expect_used)] + Err(errors + .first() + .expect("Policy check failed without an error") + .clone() + .into()) + } + }?; + + let request_type = request_env + .to_request_type() + .ok_or(EntitySliceError::PartialRequestError)?; + // Add to the manifest based on the request type. + if let Some(existing) = manifest.get_mut(&request_type) { + *existing = existing.union(&new_primary_slice); + } else { + manifest.insert(request_type, new_primary_slice); + } + } + } + + Ok(EntityManifest { + per_action: manifest, + }) +} + +/// A static analysis on type-annotated cedar expressions. +/// Computes the [`RootAccessTrie`] representing all the data required +/// to evaluate the expression. +fn compute_root_trie( + expr: &Expr>, + policy_id: &PolicyID, +) -> Result { + let mut primary_slice = RootAccessTrie::new(); + add_to_root_trie(&mut primary_slice, expr, policy_id, false)?; + Ok(primary_slice) +} + +/// Add the expression's requested data to the [`RootAccessTrie`]. +/// This handles s from the grammar (see [`FailedAnalysisError`]) +/// while [`get_expr_path`] handles the s. +fn add_to_root_trie( + root_trie: &mut RootAccessTrie, + expr: &Expr>, + policy_id: &PolicyID, + should_load_all: bool, +) -> Result<(), EntitySliceError> { + match expr.expr_kind() { + // Literals, variables, and unkonwns without any GetAttr operations + // on them are okay, since no fields need to be loaded. + ExprKind::Lit(_) => (), + ExprKind::Var(_) => (), + ExprKind::Slot(_) => (), + ExprKind::Unknown(_) => return Err(EntitySliceError::PartialExpressionError), + ExprKind::If { + test_expr, + then_expr, + else_expr, + } => { + add_to_root_trie(root_trie, test_expr, policy_id, should_load_all)?; + add_to_root_trie(root_trie, then_expr, policy_id, should_load_all)?; + add_to_root_trie(root_trie, else_expr, policy_id, should_load_all)?; + } + ExprKind::And { left, right } => { + add_to_root_trie(root_trie, left, policy_id, should_load_all)?; + add_to_root_trie(root_trie, right, policy_id, should_load_all)?; + } + ExprKind::Or { left, right } => { + add_to_root_trie(root_trie, left, policy_id, should_load_all)?; + add_to_root_trie(root_trie, right, policy_id, should_load_all)?; + } + ExprKind::UnaryApp { op, arg } => match op { + UnaryOp::Not => add_to_root_trie(root_trie, arg, policy_id, should_load_all)?, + UnaryOp::Neg => add_to_root_trie(root_trie, arg, policy_id, should_load_all)?, + }, + ExprKind::BinaryApp { op, arg1, arg2 } => match op { + // Special case! Equality between records requires + // that we load all fields. + // This could be made more precise if we check the type. + BinaryOp::Eq => { + add_to_root_trie(root_trie, arg1, policy_id, true)?; + add_to_root_trie(root_trie, arg1, policy_id, true)?; + } + BinaryOp::In => { + // Recur normally on the rhs + add_to_root_trie(root_trie, arg2, policy_id, should_load_all)?; + + // The lhs should be a datapath expression. + let mut flat_slice = get_expr_path(arg1, policy_id)?; + flat_slice.parents_required = true; + *root_trie = root_trie.union(&flat_slice.to_primary_slice()); + } + BinaryOp::Contains | BinaryOp::ContainsAll | BinaryOp::ContainsAny => { + // Like equality, another special case for records. + add_to_root_trie(root_trie, arg1, policy_id, true)?; + add_to_root_trie(root_trie, arg2, policy_id, true)?; + } + BinaryOp::Less | BinaryOp::LessEq | BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul => { + // These operators work on values, so no special + // case is needed. + add_to_root_trie(root_trie, arg1, policy_id, should_load_all)?; + add_to_root_trie(root_trie, arg2, policy_id, should_load_all)?; + } + }, + ExprKind::ExtensionFunctionApp { fn_name: _, args } => { + // WARNING: this code assumes that extension functions + // don't take full structs as inputs. + // If they did, we would need to use logic similar to the Eq binary operator. + for arg in args.iter() { + add_to_root_trie(root_trie, arg, policy_id, should_load_all)?; + } + } + ExprKind::Like { expr, pattern: _ } => { + add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; + } + ExprKind::Is { + expr, + entity_type: _, + } => { + add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; + } + ExprKind::Set(contents) => { + for expr in &**contents { + add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; + } + } + ExprKind::Record(content) => { + for expr in content.values() { + add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; + } + } + ExprKind::HasAttr { expr, attr } => { + let mut flat_slice = get_expr_path(expr, policy_id)?; + flat_slice.path.push(attr.clone()); + *root_trie = root_trie.union(&flat_slice.to_primary_slice()); + } + ExprKind::GetAttr { .. } => { + let flat_slice = get_expr_path(expr, policy_id)?; + + // PANIC SAFETY: Successfuly typechecked expressions should always have annotated types. + #[allow(clippy::expect_used)] + let leaf_field = if should_load_all { + type_to_access_trie( + expr.data() + .as_ref() + .expect("Typechecked expression missing type"), + ) + } else { + AccessTrie::new() + }; + + *root_trie = flat_slice.to_primary_slice_with_leaf(leaf_field); + } + }; + + Ok(()) +} + +/// Compute the full [`AccessTrie`] required for the type. +fn type_to_access_trie(ty: &Type) -> AccessTrie { + match ty { + // if it's not an entity or record, slice ends here + Type::ExtensionType { .. } + | Type::Never + | Type::True + | Type::False + | Type::Primitive { .. } + | Type::Set { .. } => AccessTrie::new(), + Type::EntityOrRecord(record_type) => entity_or_record_to_access_trie(record_type), + } +} + +/// Compute the full [`AccessTrie`] for the given entity or record type. +fn entity_or_record_to_access_trie(ty: &EntityRecordKind) -> AccessTrie { + match ty { + EntityRecordKind::ActionEntity { name: _, attrs } + | EntityRecordKind::Record { + attrs, + open_attributes: _, + } => { + let mut fields = HashMap::new(); + for (attr_name, attr_type) in attrs.iter() { + fields.insert( + attr_name.clone(), + Box::new(type_to_access_trie(&attr_type.attr_type)), + ); + } + AccessTrie { + children: fields, + parents_required: false, + data: (), + } + } + + EntityRecordKind::Entity(_) | EntityRecordKind::AnyEntity => { + // no need to load data for entities, which are compared + // using ids + AccessTrie::new() + } + } +} + +/// Given an expression, get the corresponding data path +/// starting with a variable. +/// If the expression is not a ``, return an error. +/// See [`FailedAnalysisError`] for more information. +fn get_expr_path( + expr: &Expr>, + policy_id: &PolicyID, +) -> Result { + Ok(match expr.expr_kind() { + ExprKind::Slot(slot_id) => { + if slot_id.is_principal() { + AccessPath { + root: EntityRoot::Var(Var::Principal), + path: vec![], + parents_required: false, + } + } else { + assert!(slot_id.is_resource()); + AccessPath { + root: EntityRoot::Var(Var::Resource), + path: vec![], + parents_required: false, + } + } + } + ExprKind::Var(var) => AccessPath { + root: EntityRoot::Var(*var), + path: vec![], + parents_required: false, + }, + ExprKind::GetAttr { expr, attr } => { + let mut slice = get_expr_path(expr, policy_id)?; + slice.path.push(attr.clone()); + slice + } + ExprKind::Lit(Literal::EntityUID(literal)) => AccessPath { + root: EntityRoot::Literal((**literal).clone()), + path: vec![], + parents_required: false, + }, + ExprKind::Unknown(_) => Err(EntitySliceError::PartialExpressionError)?, + // all other variants of expressions result in failure to analyze. + _ => Err(EntitySliceError::FailedAnalysis(FailedAnalysisError { + source_loc: expr.source_loc().cloned(), + policy_id: policy_id.clone(), + expr_kind: expr.expr_kind().clone(), + }))?, + }) +} + +#[cfg(test)] +mod entity_slice_tests { + use cedar_policy_core::{ast::PolicyID, extensions::Extensions, parser::parse_policy}; + + use super::*; + + #[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 = ValidatorSchema::from_str_natural( + " +entity User = { + name: String, +}; + +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 expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "name", + { + "children": [], + "parents_required": false + } + ] + ], + "parents_required": false + } + ] + ] + } + ] + ] +}"#; + let expected_manifest = serde_json::from_str(expected).unwrap(); + assert_eq!(entity_manifest, expected_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 = ValidatorSchema::from_str_natural( + " +entity User = { + name: String, +}; + +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 expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + ] + } + ] + ] +}"#; + let expected_manifest = serde_json::from_str(expected).unwrap(); + assert_eq!(entity_manifest, expected_manifest); + } + + #[test] + fn test_entity_manifest_parents_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_str_natural( + " +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 expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "manager", + { + "children": [], + "parents_required": true + } + ] + ], + "parents_required": true + } + ] + ] + } + ] + ] +}"#; + let expected_manifest = serde_json::from_str(expected).unwrap(); + assert_eq!(entity_manifest, expected_manifest); + } + + #[test] + fn test_entity_manifest_multiple_types() { + 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 = ValidatorSchema::from_str_natural( + " +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 entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); + let expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "name", + { + "children": [], + "parents_required": false + } + ] + ], + "parents_required": false + } + ] + ] + } + ], + [ + { + "principal": "OtherUserType", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "name", + { + "children": [], + "parents_required": false + } + ] + ], + "parents_required": false + } + ] + ] + } + ] + ] +}"#; + let expected_manifest = serde_json::from_str(expected).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_str_natural( + " +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 expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "resource" + }, + { + "children": [ + [ + "metadata", + { + "children": [ + [ + "owner", + { + "children": [], + "parents_required": false + } + ] + ], + "parents_required": false + } + ], + [ + "readers", + { + "children": [], + "parents_required": false + } + ] + ], + "parents_required": false + } + ] + ] + } + ] + ] +}"#; + let expected_manifest = serde_json::from_str(expected).unwrap(); + assert_eq!(entity_manifest, expected_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_str_natural( + " +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"); + let expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "BeSad" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "metadata", + { + "children": [ + [ + "nickname", + { + "children": [], + "parents_required": false + } + ], + [ + "friends", + { + "children": [], + "parents_required": false + } + ] + ], + "parents_required": false + } + ] + ], + "parents_required": false + } + ] + ] + } + ] + ] +}"#; + let expected_manifest = serde_json::from_str(expected).unwrap(); + assert_eq!(entity_manifest, expected_manifest); + } +} From f5e5c6531f6b142269393e3f9a4db2fb68d1aa63 Mon Sep 17 00:00:00 2001 From: oflatt Date: Wed, 31 Jul 2024 13:21:42 -0700 Subject: [PATCH 24/56] re-name entity manifest error Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 18 +++++++++--------- cedar-policy/src/api.rs | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index 3e9919317e..ec9617a3dd 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -175,7 +175,7 @@ impl Diagnostic for FailedAnalysisError { /// of Cedar handled by entity slicing. #[derive(Debug, Error, Diagnostic)] #[non_exhaustive] -pub enum EntitySliceError { +pub enum EntityManifestError { /// A validation error was encountered #[error(transparent)] #[diagnostic(transparent)] @@ -306,7 +306,7 @@ impl AccessTrie { pub fn compute_entity_manifest( schema: &ValidatorSchema, policies: &PolicySet, -) -> Result { +) -> Result { let mut manifest: HashMap = HashMap::new(); // now, for each policy we add the data it requires to the manifest @@ -343,7 +343,7 @@ pub fn compute_entity_manifest( let request_type = request_env .to_request_type() - .ok_or(EntitySliceError::PartialRequestError)?; + .ok_or(EntityManifestError::PartialRequestError)?; // Add to the manifest based on the request type. if let Some(existing) = manifest.get_mut(&request_type) { *existing = existing.union(&new_primary_slice); @@ -364,7 +364,7 @@ pub fn compute_entity_manifest( fn compute_root_trie( expr: &Expr>, policy_id: &PolicyID, -) -> Result { +) -> Result { let mut primary_slice = RootAccessTrie::new(); add_to_root_trie(&mut primary_slice, expr, policy_id, false)?; Ok(primary_slice) @@ -378,14 +378,14 @@ fn add_to_root_trie( expr: &Expr>, policy_id: &PolicyID, should_load_all: bool, -) -> Result<(), EntitySliceError> { +) -> Result<(), EntityManifestError> { match expr.expr_kind() { // Literals, variables, and unkonwns without any GetAttr operations // on them are okay, since no fields need to be loaded. ExprKind::Lit(_) => (), ExprKind::Var(_) => (), ExprKind::Slot(_) => (), - ExprKind::Unknown(_) => return Err(EntitySliceError::PartialExpressionError), + ExprKind::Unknown(_) => return Err(EntityManifestError::PartialExpressionError), ExprKind::If { test_expr, then_expr, @@ -541,7 +541,7 @@ fn entity_or_record_to_access_trie(ty: &EntityRecordKind) -> AccessTrie { fn get_expr_path( expr: &Expr>, policy_id: &PolicyID, -) -> Result { +) -> Result { Ok(match expr.expr_kind() { ExprKind::Slot(slot_id) => { if slot_id.is_principal() { @@ -574,9 +574,9 @@ fn get_expr_path( path: vec![], parents_required: false, }, - ExprKind::Unknown(_) => Err(EntitySliceError::PartialExpressionError)?, + ExprKind::Unknown(_) => Err(EntityManifestError::PartialExpressionError)?, // all other variants of expressions result in failure to analyze. - _ => Err(EntitySliceError::FailedAnalysis(FailedAnalysisError { + _ => Err(EntityManifestError::FailedAnalysis(FailedAnalysisError { source_loc: expr.source_loc().cloned(), policy_id: policy_id.clone(), expr_kind: expr.expr_kind().clone(), diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index c47e3cb2e0..3a5919cf5a 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -27,7 +27,7 @@ mod id; use cedar_policy_validator::entity_manifest; #[cfg(feature = "entity-manifest")] pub use cedar_policy_validator::entity_manifest::{ - AccessTrie, EntityManifest, EntityRoot, EntitySliceError, Fields, RootAccessTrie, + AccessTrie, EntityManifest, EntityManifestError, EntityRoot, Fields, RootAccessTrie, }; use cedar_policy_validator::typecheck::{PolicyCheck, Typechecker}; pub use id::*; @@ -4288,6 +4288,6 @@ action CreateList in Create appliesTo { pub fn compute_entity_manifest( schema: &Schema, pset: &PolicySet, -) -> Result { +) -> Result { entity_manifest::compute_entity_manifest(&schema.0, &pset.ast) } From 45b9c72a9cf9ccfacd6393fc05dccbbfda5a4410 Mon Sep 17 00:00:00 2001 From: Oliver Flatt Date: Thu, 1 Aug 2024 10:45:23 -0700 Subject: [PATCH 25/56] Update cedar-policy-core/src/ast/request.rs Co-authored-by: Craig Disselkoen Signed-off-by: oflatt --- cedar-policy-core/src/ast/request.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cedar-policy-core/src/ast/request.rs b/cedar-policy-core/src/ast/request.rs index 5d46b1c667..136ac1db24 100644 --- a/cedar-policy-core/src/ast/request.rs +++ b/cedar-policy-core/src/ast/request.rs @@ -49,7 +49,7 @@ pub struct Request { pub(crate) context: Option, } -/// Represents the principal, action, and resource types. +/// Represents the principal type, resource type, and action UID. #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] pub struct RequestType { /// Principal type From dda91d0f9f846b967c68ebda2fff6af461ab027d Mon Sep 17 00:00:00 2001 From: Oliver Flatt Date: Thu, 1 Aug 2024 10:45:31 -0700 Subject: [PATCH 26/56] Update cedar-policy-core/src/ast/request.rs Co-authored-by: Craig Disselkoen Signed-off-by: oflatt --- cedar-policy-core/src/ast/request.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cedar-policy-core/src/ast/request.rs b/cedar-policy-core/src/ast/request.rs index 136ac1db24..0928251b66 100644 --- a/cedar-policy-core/src/ast/request.rs +++ b/cedar-policy-core/src/ast/request.rs @@ -54,7 +54,7 @@ pub struct Request { pub struct RequestType { /// Principal type pub principal: EntityType, - /// Aciton type + /// Action type pub action: EntityUID, /// Resource type pub resource: EntityType, From c44475807aa4b55d3546c60e59542cb5a1d58de5 Mon Sep 17 00:00:00 2001 From: Oliver Flatt Date: Thu, 1 Aug 2024 10:50:35 -0700 Subject: [PATCH 27/56] Update cedar-policy-validator/src/entity_manifest.rs Co-authored-by: Craig Disselkoen Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index ec9617a3dd..683eea2dd9 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -164,7 +164,7 @@ impl Diagnostic for FailedAnalysisError { fn help<'a>(&'a self) -> Option> { Some(Box::new(format!( - "Entity slicing failed to analyze expression: {} operators are not allowed before accessing record or entity attributes.", + "failed to compute entity manifest: {} operators are not allowed before accessing record or entity attributes", self.expr_kind.operator_description() ))) } From 7528111c7386b093730897c60f2ccf29f0840fc9 Mon Sep 17 00:00:00 2001 From: Oliver Flatt Date: Thu, 1 Aug 2024 11:02:00 -0700 Subject: [PATCH 28/56] Update cedar-policy-validator/src/entity_manifest.rs Co-authored-by: Craig Disselkoen Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index 683eea2dd9..295776e621 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -323,7 +323,7 @@ pub fn compute_entity_manifest( compute_root_trie(&typechecked_expr, policy.id()) } PolicyCheck::Irrelevant(_) => { - // this policy is ireelevant, so we need no data + // this policy is irrelevant, so we need no data Ok(RootAccessTrie::new()) } From 1a49a4c4fb73fc2ca43fa69f77f164b36f8efd16 Mon Sep 17 00:00:00 2001 From: Oliver Flatt Date: Thu, 1 Aug 2024 11:11:20 -0700 Subject: [PATCH 29/56] Update cedar-policy/src/api.rs Co-authored-by: Craig Disselkoen Signed-off-by: oflatt --- cedar-policy/src/api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 3a5919cf5a..8af4faf4b9 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -4278,7 +4278,7 @@ action CreateList in Create appliesTo { } } -/// Given a schema and policy set, compute an entity slice manifest. +/// Given a schema and policy set, compute an entity manifest. /// The policies must validate against the schema in strict mode, /// otherwise an error is returned. /// The manifest describes the data required to answer requests From a736421309af1df74add2871eb3259b21e7c430e Mon Sep 17 00:00:00 2001 From: Oliver Flatt Date: Thu, 1 Aug 2024 11:11:27 -0700 Subject: [PATCH 30/56] Update cedar-policy/src/api.rs Co-authored-by: Craig Disselkoen Signed-off-by: oflatt --- cedar-policy/src/api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 8af4faf4b9..cc70618daf 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -4282,7 +4282,7 @@ action CreateList in Create appliesTo { /// The policies must validate against the schema in strict mode, /// otherwise an error is returned. /// The manifest describes the data required to answer requests -/// for each action type. +/// for each action. #[doc = include_str!("../experimental_warning.md")] #[cfg(feature = "entity-manifest")] pub fn compute_entity_manifest( From 84545d7aad9915590ec5319cbc7f76e0005db0f3 Mon Sep 17 00:00:00 2001 From: oflatt Date: Thu, 1 Aug 2024 11:15:42 -0700 Subject: [PATCH 31/56] Respond to @cdisselkoen PR feedback Signed-off-by: oflatt --- cedar-policy-core/src/ast/request.rs | 4 +- cedar-policy-validator/src/entity_manifest.rs | 258 ++++++++++-------- cedar-policy/CHANGELOG.md | 5 +- cedar-policy/src/api.rs | 2 +- 4 files changed, 145 insertions(+), 124 deletions(-) diff --git a/cedar-policy-core/src/ast/request.rs b/cedar-policy-core/src/ast/request.rs index 0928251b66..def22fcbb0 100644 --- a/cedar-policy-core/src/ast/request.rs +++ b/cedar-policy-core/src/ast/request.rs @@ -205,9 +205,9 @@ impl Request { /// Returns `None` if the request is not fully concrete. pub fn to_request_type(&self) -> Option { Some(RequestType { - principal: self.principal.uid()?.clone().components().0, + principal: self.principal.uid()?.entity_type().clone(), action: self.action.uid()?.clone(), - resource: self.resource().uid()?.clone().components().0, + resource: self.resource().uid()?.entity_type().clone(), }) } } diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index 295776e621..c198a67d8e 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -42,6 +42,9 @@ use crate::{ /// based on the the [`RequestType`]. /// For each request type, the [`EntityManifest`] stores /// a [`RootAccessTrie`] of data to retrieve. +/// +/// `T` represents an optional type annotation on each +/// node in the [`AccessTrie`]. #[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -61,7 +64,7 @@ where #[doc = include_str!("../../cedar-policy/experimental_warning.md")] pub type Fields = HashMap>>; -/// The root of an entity slice. +/// The root of a data path or [`RootAccessTrie`]. #[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub enum EntityRoot { @@ -80,12 +83,15 @@ impl Display for EntityRoot { } } -/// A [`RootAccessTrie`] is a trie describing +/// A [`RootAccessTrie`] is a trie describing a set of /// data paths to retrieve. Each edge in the trie /// is either a record or entity dereference. /// /// If an entity or record field does not exist in the backing store, /// it is safe to stop loading data at that point. +/// +/// `T` represents an optional type annotation on each +/// node in the [`AccessTrie`]. #[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -93,13 +99,17 @@ pub struct RootAccessTrie where T: Clone, { + /// The data that needs to be loaded, organized by root. #[serde_as(as = "Vec<(_, _)>")] #[serde(bound(deserialize = "T: Default"))] - /// The data that needs to be loaded, organized by root. pub trie: HashMap>, } -/// An entity slice- tells users a tree of data to load +/// A Trie representing a set of data paths to load, +/// starting implicitly from a Cedar value. +/// +/// `T` represents an optional type annotation on each +/// node in the [`AccessTrie`]. #[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -109,9 +119,9 @@ pub struct AccessTrie { #[serde_as(as = "Vec<(_, _)>")] pub children: Fields, /// For entity types, this boolean may be `true` - /// to signal that all the parents in the entity hierarchy + /// to signal that all the ancestors in the entity hierarchy /// are required (transitively). - pub parents_required: bool, + pub ancestors_required: bool, /// Optional data annotation, usually used for type information. #[serde(skip_serializing, skip_deserializing)] #[serde(bound(deserialize = "T: Default"))] @@ -127,7 +137,7 @@ struct AccessPath { /// The path of fields of entities or structs pub path: Vec, /// Request all the parents in the entity hierarchy of this entity. - pub parents_required: bool, + pub ancestors_required: bool, } /// Entity manifest computation does not handle the full @@ -170,6 +180,23 @@ impl Diagnostic for FailedAnalysisError { } } +/// Error when expressions are partial during entity +/// manifest computation +#[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 {} + +/// Error when the request is partial during entity +/// manifest computation +#[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 {} + /// An error generated by entity slicing. /// See [`FailedAnalysisError`] for details on the fragment /// of Cedar handled by entity slicing. @@ -186,13 +213,13 @@ pub enum EntityManifestError { EntitiesError(#[from] EntitiesError), /// The request was partial - #[error("Entity slicing requires a fully concrete request. Got a partial request.")] - PartialRequestError, + #[error(transparent)] + #[diagnostic(transparent)] + PartialRequestError(#[from] PartialRequestError), /// A policy was partial - #[error( - "Entity slicing requires fully concrete policies. Got a policy with an unknown expression." - )] - PartialExpressionError, + #[error(transparent)] + #[diagnostic(transparent)] + PartialExpressionError(#[from] PartialExpressionError), /// A policy was not analyzable because it used unsupported operators /// before a [`ExprKind::GetAttr`] @@ -206,20 +233,18 @@ pub enum EntityManifestError { fn union_fields(first: &Fields, second: &Fields) -> Fields { let mut res = first.clone(); for (key, value) in second { - if let Some(existing) = res.get(key) { - res.insert(key.clone(), Box::new((*existing).union(value))); - } else { - res.insert(key.clone(), value.clone()); - } + res.entry(key.clone()) + .and_modify(|existing| *existing = Box::new((*existing).union(value))) + .or_insert(value.clone()); } res } impl AccessPath { /// Convert a [`AccessPath`] into corresponding [`RootAccessTrie`]. - fn to_primary_slice(&self) -> RootAccessTrie { - self.to_primary_slice_with_leaf(AccessTrie { - parents_required: true, + fn to_root_access_trie(&self) -> RootAccessTrie { + self.to_root_access_trie_with_leaf(AccessTrie { + ancestors_required: true, children: Default::default(), data: (), }) @@ -227,14 +252,14 @@ impl AccessPath { /// Convert an [`AccessPath`] to a [`RootAccessTrie`], and also /// add a full trie as the leaf at the end. - fn to_primary_slice_with_leaf(&self, leaf_trie: AccessTrie) -> RootAccessTrie { + fn to_root_access_trie_with_leaf(&self, leaf_trie: AccessTrie) -> RootAccessTrie { let mut current = leaf_trie; // reverse the path, visiting the last access first for field in self.path.iter().rev() { let mut fields = HashMap::new(); fields.insert(field.clone(), Box::new(current)); current = AccessTrie { - parents_required: false, + ancestors_required: false, children: fields, data: (), }; @@ -261,11 +286,10 @@ impl RootAccessTrie { fn union(&self, other: &Self) -> Self { let mut res = self.clone(); for (key, value) in &other.trie { - if let Some(existing) = res.trie.get(key) { - res.trie.insert(key.clone(), (*existing).union(value)); - } else { - res.trie.insert(key.clone(), value.clone()); - } + res.trie + .entry(key.clone()) + .and_modify(|existing| *existing = (*existing).union(value)) + .or_insert(value.clone()); } res } @@ -283,7 +307,7 @@ impl AccessTrie { fn union(&self, other: &Self) -> Self { Self { children: union_fields(&self.children, &other.children), - parents_required: self.parents_required || other.parents_required, + ancestors_required: self.ancestors_required || other.ancestors_required, data: self.data.clone(), } } @@ -294,7 +318,7 @@ impl AccessTrie { fn new() -> Self { Self { children: Default::default(), - parents_required: false, + ancestors_required: false, data: (), } } @@ -315,7 +339,6 @@ pub fn compute_entity_manifest( let typechecker = Typechecker::new(schema, ValidationMode::Strict, policy.id().clone()); let request_envs = typechecker.typecheck_by_request_env(policy.template()); for (request_env, policy_check) in request_envs { - // match on the typechecking answer let new_primary_slice = match policy_check { PolicyCheck::Success(typechecked_expr) => { // compute the trie from the typechecked expr @@ -343,13 +366,14 @@ pub fn compute_entity_manifest( let request_type = request_env .to_request_type() - .ok_or(EntityManifestError::PartialRequestError)?; + .ok_or(PartialRequestError {})?; // Add to the manifest based on the request type. - if let Some(existing) = manifest.get_mut(&request_type) { - *existing = existing.union(&new_primary_slice); - } else { - manifest.insert(request_type, new_primary_slice); - } + manifest + .entry(request_type) + .and_modify(|existing| { + *existing = existing.union(&new_primary_slice); + }) + .or_insert(new_primary_slice); } } @@ -382,10 +406,10 @@ fn add_to_root_trie( match expr.expr_kind() { // Literals, variables, and unkonwns without any GetAttr operations // on them are okay, since no fields need to be loaded. - ExprKind::Lit(_) => (), - ExprKind::Var(_) => (), - ExprKind::Slot(_) => (), - ExprKind::Unknown(_) => return Err(EntityManifestError::PartialExpressionError), + ExprKind::Lit(_) => Ok(()), + ExprKind::Var(_) => Ok(()), + ExprKind::Slot(_) => Ok(()), + ExprKind::Unknown(_) => Err(PartialExpressionError {})?, ExprKind::If { test_expr, then_expr, @@ -394,26 +418,33 @@ fn add_to_root_trie( add_to_root_trie(root_trie, test_expr, policy_id, should_load_all)?; add_to_root_trie(root_trie, then_expr, policy_id, should_load_all)?; add_to_root_trie(root_trie, else_expr, policy_id, should_load_all)?; + Ok(()) } ExprKind::And { left, right } => { add_to_root_trie(root_trie, left, policy_id, should_load_all)?; add_to_root_trie(root_trie, right, policy_id, should_load_all)?; + Ok(()) } ExprKind::Or { left, right } => { add_to_root_trie(root_trie, left, policy_id, should_load_all)?; add_to_root_trie(root_trie, right, policy_id, should_load_all)?; + Ok(()) + } + ExprKind::UnaryApp { op, arg } => { + match op { + UnaryOp::Not => add_to_root_trie(root_trie, arg, policy_id, should_load_all)?, + UnaryOp::Neg => add_to_root_trie(root_trie, arg, policy_id, should_load_all)?, + }; + Ok(()) } - ExprKind::UnaryApp { op, arg } => match op { - UnaryOp::Not => add_to_root_trie(root_trie, arg, policy_id, should_load_all)?, - UnaryOp::Neg => add_to_root_trie(root_trie, arg, policy_id, should_load_all)?, - }, ExprKind::BinaryApp { op, arg1, arg2 } => match op { // Special case! Equality between records requires // that we load all fields. // This could be made more precise if we check the type. BinaryOp::Eq => { add_to_root_trie(root_trie, arg1, policy_id, true)?; - add_to_root_trie(root_trie, arg1, policy_id, true)?; + add_to_root_trie(root_trie, arg2, policy_id, true)?; + Ok(()) } BinaryOp::In => { // Recur normally on the rhs @@ -421,19 +452,22 @@ fn add_to_root_trie( // The lhs should be a datapath expression. let mut flat_slice = get_expr_path(arg1, policy_id)?; - flat_slice.parents_required = true; - *root_trie = root_trie.union(&flat_slice.to_primary_slice()); + flat_slice.ancestors_required = true; + *root_trie = root_trie.union(&flat_slice.to_root_access_trie()); + Ok(()) } BinaryOp::Contains | BinaryOp::ContainsAll | BinaryOp::ContainsAny => { // Like equality, another special case for records. add_to_root_trie(root_trie, arg1, policy_id, true)?; add_to_root_trie(root_trie, arg2, policy_id, true)?; + Ok(()) } BinaryOp::Less | BinaryOp::LessEq | BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul => { - // These operators work on values, so no special + // These operators work on literals, so no special // case is needed. add_to_root_trie(root_trie, arg1, policy_id, should_load_all)?; add_to_root_trie(root_trie, arg2, policy_id, should_load_all)?; + Ok(()) } }, ExprKind::ExtensionFunctionApp { fn_name: _, args } => { @@ -443,30 +477,36 @@ fn add_to_root_trie( for arg in args.iter() { add_to_root_trie(root_trie, arg, policy_id, should_load_all)?; } + Ok(()) } ExprKind::Like { expr, pattern: _ } => { add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; + Ok(()) } ExprKind::Is { expr, entity_type: _, } => { add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; + Ok(()) } ExprKind::Set(contents) => { for expr in &**contents { add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; } + Ok(()) } ExprKind::Record(content) => { for expr in content.values() { add_to_root_trie(root_trie, expr, policy_id, should_load_all)?; } + Ok(()) } ExprKind::HasAttr { expr, attr } => { let mut flat_slice = get_expr_path(expr, policy_id)?; flat_slice.path.push(attr.clone()); - *root_trie = root_trie.union(&flat_slice.to_primary_slice()); + *root_trie = root_trie.union(&flat_slice.to_root_access_trie()); + Ok(()) } ExprKind::GetAttr { .. } => { let flat_slice = get_expr_path(expr, policy_id)?; @@ -483,11 +523,10 @@ fn add_to_root_trie( AccessTrie::new() }; - *root_trie = flat_slice.to_primary_slice_with_leaf(leaf_field); + *root_trie = flat_slice.to_root_access_trie_with_leaf(leaf_field); + Ok(()) } - }; - - Ok(()) + } } /// Compute the full [`AccessTrie`] required for the type. @@ -507,11 +546,7 @@ fn type_to_access_trie(ty: &Type) -> AccessTrie { /// Compute the full [`AccessTrie`] for the given entity or record type. fn entity_or_record_to_access_trie(ty: &EntityRecordKind) -> AccessTrie { match ty { - EntityRecordKind::ActionEntity { name: _, attrs } - | EntityRecordKind::Record { - attrs, - open_attributes: _, - } => { + EntityRecordKind::ActionEntity { attrs, .. } | EntityRecordKind::Record { attrs, .. } => { let mut fields = HashMap::new(); for (attr_name, attr_type) in attrs.iter() { fields.insert( @@ -521,7 +556,7 @@ fn entity_or_record_to_access_trie(ty: &EntityRecordKind) -> AccessTrie { } AccessTrie { children: fields, - parents_required: false, + ancestors_required: false, data: (), } } @@ -548,21 +583,21 @@ fn get_expr_path( AccessPath { root: EntityRoot::Var(Var::Principal), path: vec![], - parents_required: false, + ancestors_required: false, } } else { assert!(slot_id.is_resource()); AccessPath { root: EntityRoot::Var(Var::Resource), path: vec![], - parents_required: false, + ancestors_required: false, } } } ExprKind::Var(var) => AccessPath { root: EntityRoot::Var(*var), path: vec![], - parents_required: false, + ancestors_required: false, }, ExprKind::GetAttr { expr, attr } => { let mut slice = get_expr_path(expr, policy_id)?; @@ -572,9 +607,9 @@ fn get_expr_path( ExprKind::Lit(Literal::EntityUID(literal)) => AccessPath { root: EntityRoot::Literal((**literal).clone()), path: vec![], - parents_required: false, + ancestors_required: false, }, - ExprKind::Unknown(_) => Err(EntityManifestError::PartialExpressionError)?, + ExprKind::Unknown(_) => Err(PartialExpressionError {})?, // all other variants of expressions result in failure to analyze. _ => Err(EntityManifestError::FailedAnalysis(FailedAnalysisError { source_loc: expr.source_loc().cloned(), @@ -590,20 +625,9 @@ mod entity_slice_tests { use super::*; - #[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 = ValidatorSchema::from_str_natural( + // Schema for testing in this module + fn schema() -> ValidatorSchema { + ValidatorSchema::from_str_natural( " entity User = { name: String, @@ -615,11 +639,27 @@ action Read appliesTo { principal: [User], resource: [Document] }; - ", + ", Extensions::all_available(), ) .unwrap() - .0; + .0 + } + + #[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 expected = r#" @@ -646,11 +686,11 @@ action Read appliesTo { "name", { "children": [], - "parents_required": false + "ancestors_required": false } ] ], - "parents_required": false + "ancestors_required": false } ] ] @@ -669,23 +709,7 @@ action Read appliesTo { parse_policy(None, "permit(principal, action, resource);").expect("should succeed"); pset.add(policy.into()).expect("should succeed"); - let schema = ValidatorSchema::from_str_natural( - " -entity User = { - name: String, -}; - -entity Document; - -action Read appliesTo { - principal: [User], - resource: [Document] -}; - ", - Extensions::all_available(), - ) - .unwrap() - .0; + let schema = schema(); let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = r#" @@ -712,7 +736,7 @@ action Read appliesTo { } #[test] - fn test_entity_manifest_parents_required() { + fn test_entity_manifest_ancestors_required() { let mut pset = PolicySet::new(); let policy = parse_policy( None, @@ -730,14 +754,12 @@ entity User in [Document] = { name: String, manager: User }; - entity Document; - action Read appliesTo { principal: [User], resource: [Document] }; - ", + ", Extensions::all_available(), ) .unwrap() @@ -768,11 +790,11 @@ action Read appliesTo { "manager", { "children": [], - "parents_required": true + "ancestors_required": true } ] ], - "parents_required": true + "ancestors_required": true } ] ] @@ -845,11 +867,11 @@ action Read appliesTo { "name", { "children": [], - "parents_required": false + "ancestors_required": false } ] ], - "parents_required": false + "ancestors_required": false } ] ] @@ -876,11 +898,11 @@ action Read appliesTo { "name", { "children": [], - "parents_required": false + "ancestors_required": false } ] ], - "parents_required": false + "ancestors_required": false } ] ] @@ -978,22 +1000,22 @@ action Read appliesTo { "owner", { "children": [], - "parents_required": false + "ancestors_required": false } ] ], - "parents_required": false + "ancestors_required": false } ], [ "readers", { "children": [], - "parents_required": false + "ancestors_required": false } ] ], - "parents_required": false + "ancestors_required": false } ] ] @@ -1074,22 +1096,22 @@ action BeSad appliesTo { "nickname", { "children": [], - "parents_required": false + "ancestors_required": false } ], [ "friends", { "children": [], - "parents_required": false + "ancestors_required": false } ] ], - "parents_required": false + "ancestors_required": false } ] ], - "parents_required": false + "ancestors_required": false } ] ] diff --git a/cedar-policy/CHANGELOG.md b/cedar-policy/CHANGELOG.md index 6816b757e7..8d541d38d2 100644 --- a/cedar-policy/CHANGELOG.md +++ b/cedar-policy/CHANGELOG.md @@ -13,11 +13,10 @@ Starting with version 3.2.4, changes marked with a star (*) are _language breaki Cedar Language Version: 4.0 ### Added -- A new experimental API (`compute_entity_manifest`) +- Implemented [RFC 74](https://github.com/cedar-policy/rfcs/pull/74): A new experimental API (`compute_entity_manifest`) that provides the Entity Manifest: a data structure that describes what data is required to satisfy a - Cedar request. Entity Manifests improve Cedar performance dramatically - in a safe way. + Cedar request. - JSON representation for Policy Sets, along with methods like `::from_json_value/file/str` and `::to_json` for `PolicySet`. (#783, resolving #549) diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index cc70618daf..b27adc2f24 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -37,9 +37,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}; From effb671911686c7e9b38b256788a22db01edc9c9 Mon Sep 17 00:00:00 2001 From: oflatt Date: Thu, 1 Aug 2024 11:40:25 -0700 Subject: [PATCH 32/56] caught bug with new test case Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 127 +++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index c198a67d8e..e83edab632 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -523,7 +523,7 @@ fn add_to_root_trie( AccessTrie::new() }; - *root_trie = flat_slice.to_root_access_trie_with_leaf(leaf_field); + *root_trie = root_trie.union(&flat_slice.to_root_access_trie_with_leaf(leaf_field)); Ok(()) } } @@ -1118,6 +1118,131 @@ action BeSad appliesTo { } ] ] +}"#; + let expected_manifest = serde_json::from_str(expected).unwrap(); + assert_eq!(entity_manifest, expected_manifest); + } + + #[test] + fn test_entity_manifest_struct_equality_left_right_different() { + 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 == resource.metadata +};"#, + ) + .expect("should succeed"); + pset.add(policy.into()).expect("should succeed"); + + let schema = ValidatorSchema::from_str_natural( + " +entity User = { + name: String, + metadata: { + friends: Set, + nickname: String, + }, +}; + +entity Document; + +action Hello appliesTo { + principal: [User], + resource: [User] +}; + ", + Extensions::all_available(), + ) + .unwrap() + .0; + + let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); + let expected = r#" +{ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Hello" + }, + "resource": "User" + }, + { + "trie": [ + [ + { + "Var": "resource" + }, + { + "children": [ + [ + "metadata", + { + "children": [ + [ + "friends", + { + "children": [], + "ancestors_required": false + } + ], + [ + "nickname", + { + "children": [], + "ancestors_required": false + } + ] + ], + "ancestors_required": false + } + ] + ], + "ancestors_required": false + } + ], + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "metadata", + { + "children": [ + [ + "nickname", + { + "children": [], + "ancestors_required": false + } + ], + [ + "friends", + { + "children": [], + "ancestors_required": false + } + ] + ], + "ancestors_required": false + } + ] + ], + "ancestors_required": false + } + ] + ] + } + ] + ] }"#; let expected_manifest = serde_json::from_str(expected).unwrap(); assert_eq!(entity_manifest, expected_manifest); From 34c461bd0d7826775b162530bb107e12fb0f2b64 Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 26 Aug 2024 10:36:52 -0700 Subject: [PATCH 33/56] remove typechecking TODO Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index e83edab632..b7d52cde48 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -350,9 +350,6 @@ pub fn compute_entity_manifest( Ok(RootAccessTrie::new()) } - // TODO is returning the first error correct? - // Also, should we run full validation instead of just - // typechecking? Validation does a little more right? PolicyCheck::Fail(errors) => { // PANIC SAFETY: policy check fail should be a non-empty vector. #[allow(clippy::expect_used)] From d45421ed9b17b922c9a8a6229a59cf414a4b308c Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 26 Aug 2024 10:42:21 -0700 Subject: [PATCH 34/56] add feature to validator crate as well Signed-off-by: oflatt --- cedar-policy-validator/Cargo.toml | 1 + cedar-policy-validator/src/lib.rs | 1 + cedar-policy/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cedar-policy-validator/Cargo.toml b/cedar-policy-validator/Cargo.toml index a8f3f00b95..2a032faa62 100644 --- a/cedar-policy-validator/Cargo.toml +++ b/cedar-policy-validator/Cargo.toml @@ -47,6 +47,7 @@ arbitrary = ["dep:arbitrary", "cedar-policy-core/arbitrary"] # Experimental features. partial-validate = [] wasm = ["serde-wasm-bindgen", "tsify", "wasm-bindgen"] +entity-manifest = [] [dev-dependencies] similar-asserts = "1.5.0" diff --git a/cedar-policy-validator/src/lib.rs b/cedar-policy-validator/src/lib.rs index 97c0bcdf42..ef08a80db1 100644 --- a/cedar-policy-validator/src/lib.rs +++ b/cedar-policy-validator/src/lib.rs @@ -35,6 +35,7 @@ use cedar_policy_core::ast::{Policy, PolicySet, Template}; use serde::Serialize; use std::collections::HashSet; +#[cfg(feature = "entity-manifest")] pub mod entity_manifest; mod err; pub use err::*; diff --git a/cedar-policy/Cargo.toml b/cedar-policy/Cargo.toml index 9d91bde598..13dcd3aa81 100644 --- a/cedar-policy/Cargo.toml +++ b/cedar-policy/Cargo.toml @@ -47,7 +47,7 @@ corpus-timing = [] # Experimental features. # Enable all experimental features with `cargo build --features "experimental"` experimental = ["partial-eval", "permissive-validate", "partial-validate", "entity-manifest"] -entity-manifest = [] +entity-manifest = ["cedar-policy-validator/entity-manifest"] partial-eval = ["cedar-policy-core/partial-eval", "cedar-policy-validator/partial-eval"] permissive-validate = [] partial-validate = ["cedar-policy-validator/partial-validate"] From e0fd602aa8ac4a8d5c8706218527a566ce9dde3c Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 26 Aug 2024 10:44:11 -0700 Subject: [PATCH 35/56] move public error to err.rs file Signed-off-by: oflatt --- cedar-policy/src/api.rs | 2 +- cedar-policy/src/api/err.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index b27adc2f24..4a643e3804 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -27,7 +27,7 @@ mod id; use cedar_policy_validator::entity_manifest; #[cfg(feature = "entity-manifest")] pub use cedar_policy_validator::entity_manifest::{ - AccessTrie, EntityManifest, EntityManifestError, EntityRoot, Fields, RootAccessTrie, + AccessTrie, EntityManifest, EntityRoot, Fields, RootAccessTrie, }; use cedar_policy_validator::typecheck::{PolicyCheck, Typechecker}; pub use id::*; diff --git a/cedar-policy/src/api/err.rs b/cedar-policy/src/api/err.rs index 55ca309481..3ecc0ba8ac 100644 --- a/cedar-policy/src/api/err.rs +++ b/cedar-policy/src/api/err.rs @@ -39,6 +39,9 @@ pub mod entities_errors { pub use cedar_policy_core::entities::err::{Duplicate, EntitiesError, TransitiveClosureError}; } +/// Entity manifest errors +pub use cedar_policy_validator::entity_manifest::EntityManifestError; + /// Errors related to serializing/deserializing entities or contexts to/from JSON pub mod entities_json_errors { pub use cedar_policy_core::entities::json::err::{ From 3700d5401bf4bbb1002218e35401ddc93e589cf3 Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 26 Aug 2024 10:45:55 -0700 Subject: [PATCH 36/56] caution on pub types Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index b7d52cde48..b87254cc67 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -45,6 +45,9 @@ use crate::{ /// /// `T` represents an optional type annotation on each /// node in the [`AccessTrie`]. +// 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. #[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -61,10 +64,16 @@ where /// A map of data fields to [`AccessTrie`]s. /// The keys to this map form the edges in the access trie, /// pointing to sub-tries. +// 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. #[doc = include_str!("../../cedar-policy/experimental_warning.md")] pub type Fields = HashMap>>; /// The root of a data path or [`RootAccessTrie`]. +// 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. #[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub enum EntityRoot { @@ -92,6 +101,9 @@ impl Display for EntityRoot { /// /// `T` represents an optional type annotation on each /// node in the [`AccessTrie`]. +// 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. #[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -110,6 +122,10 @@ where /// /// `T` represents an optional type annotation on each /// node in the [`AccessTrie`]. +/// +// 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. #[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -200,6 +216,9 @@ impl Diagnostic for PartialRequestError {} /// An error generated by entity slicing. /// See [`FailedAnalysisError`] for details on the fragment /// of Cedar handled by 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, Error, Diagnostic)] #[non_exhaustive] pub enum EntityManifestError { From a3d3d372fcac95f417b9a60a1a8386cad79ed5cd Mon Sep 17 00:00:00 2001 From: oflatt Date: Mon, 26 Aug 2024 15:18:37 -0700 Subject: [PATCH 37/56] undo bad merges Signed-off-by: oflatt --- cedar-policy-validator/src/err.rs | 3 ++- cedar-policy/CHANGELOG.md | 8 -------- cedar-testing/src/cedar_test_impl.rs | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/cedar-policy-validator/src/err.rs b/cedar-policy-validator/src/err.rs index 54df6195b9..e5a798291b 100644 --- a/cedar-policy-validator/src/err.rs +++ b/cedar-policy-validator/src/err.rs @@ -14,7 +14,6 @@ * limitations under the License. */ -use crate::cedar_schema; use cedar_policy_core::{ ast::{EntityUID, ReservedNameError}, transitive_closure, @@ -24,6 +23,8 @@ use miette::Diagnostic; use nonempty::NonEmpty; use thiserror::Error; +use crate::cedar_schema; + /// Error creating a schema from the Cedar syntax #[derive(Debug, Error, Diagnostic)] pub enum CedarSchemaError { diff --git a/cedar-policy/CHANGELOG.md b/cedar-policy/CHANGELOG.md index 8d541d38d2..988f973b73 100644 --- a/cedar-policy/CHANGELOG.md +++ b/cedar-policy/CHANGELOG.md @@ -17,14 +17,6 @@ Cedar Language Version: 4.0 that provides the Entity Manifest: a data structure that describes what data is required to satisfy a Cedar request. -- JSON representation for Policy Sets, along with methods like - `::from_json_value/file/str` and `::to_json` for `PolicySet`. (#783, - resolving #549) -- Added methods for reading and writing individual `Entity`s as JSON - (resolving #807) -- `Context::into_iter` to get the contents of a `Context` and `Context::merge` - to combine `Context`s, returning an error on duplicate keys (#1027, - resolving #1013) - Additional functionality to the JSON FFI including parsing utilities (#1079) and conversion between the Cedar and JSON formats (#1087) - (*) Schema JSON syntax now accepts a type `EntityOrCommon` representing a diff --git a/cedar-testing/src/cedar_test_impl.rs b/cedar-testing/src/cedar_test_impl.rs index 1a98ea6892..7a41e823f4 100644 --- a/cedar-testing/src/cedar_test_impl.rs +++ b/cedar-testing/src/cedar_test_impl.rs @@ -20,7 +20,7 @@ //! testing (see ). pub use cedar_policy::ffi; -use cedar_policy_core::ast::PartialValue; +use cedar_policy_core::ast::{self, PartialValue}; use cedar_policy_core::ast::{Expr, PolicySet, Request, Value}; use cedar_policy_core::authorizer::Authorizer; use cedar_policy_core::entities::{Entities, TCComputation}; From cadb083ac482865a71ed1a1e4357fc55c954178c Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 27 Aug 2024 10:14:32 -0600 Subject: [PATCH 38/56] use correct error macro Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 4 ++-- cedar-policy/src/api/err.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index b87254cc67..c0fe917341 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -23,7 +23,7 @@ use cedar_policy_core::ast::{ BinaryOp, EntityUID, Expr, ExprKind, Literal, PolicyID, PolicySet, RequestType, UnaryOp, Var, }; use cedar_policy_core::entities::err::EntitiesError; -use cedar_policy_core::impl_diagnostic_from_source_loc_field; +use cedar_policy_core::impl_diagnostic_from_source_loc_opt_field; use cedar_policy_core::parser::Loc; use miette::Diagnostic; use serde::{Deserialize, Serialize}; @@ -186,7 +186,7 @@ pub struct FailedAnalysisError { } impl Diagnostic for FailedAnalysisError { - impl_diagnostic_from_source_loc_field!(); + impl_diagnostic_from_source_loc_opt_field!(source_loc); fn help<'a>(&'a self) -> Option> { Some(Box::new(format!( diff --git a/cedar-policy/src/api/err.rs b/cedar-policy/src/api/err.rs index 3ecc0ba8ac..e86b99855b 100644 --- a/cedar-policy/src/api/err.rs +++ b/cedar-policy/src/api/err.rs @@ -40,6 +40,7 @@ pub mod entities_errors { } /// Entity manifest errors +#[cfg(feature = "entity-manifest")] pub use cedar_policy_validator::entity_manifest::EntityManifestError; /// Errors related to serializing/deserializing entities or contexts to/from JSON From c3d2991c9024016937a98e2b8b8952857fe63dc1 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 27 Aug 2024 10:48:24 -0600 Subject: [PATCH 39/56] cedar schema str fn Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index c0fe917341..40451a0034 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -643,7 +643,7 @@ mod entity_slice_tests { // Schema for testing in this module fn schema() -> ValidatorSchema { - ValidatorSchema::from_str_natural( + ValidatorSchema::from_cedarschema_str( " entity User = { name: String, @@ -764,7 +764,7 @@ when { .expect("should succeed"); pset.add(policy.into()).expect("should succeed"); - let schema = ValidatorSchema::from_str_natural( + let schema = ValidatorSchema::from_cedarschema_str( " entity User in [Document] = { name: String, @@ -835,7 +835,7 @@ when { .expect("should succeed"); pset.add(policy.into()).expect("should succeed"); - let schema = ValidatorSchema::from_str_natural( + let schema = ValidatorSchema::from_cedarschema_str( " entity User = { name: String, @@ -963,7 +963,7 @@ when pset.add(policy1.into()).expect("should succeed"); pset.add(policy2.into()).expect("should succeed"); - let schema = ValidatorSchema::from_str_natural( + let schema = ValidatorSchema::from_cedarschema_str( " entity User; @@ -1061,7 +1061,7 @@ when { .expect("should succeed"); pset.add(policy.into()).expect("should succeed"); - let schema = ValidatorSchema::from_str_natural( + let schema = ValidatorSchema::from_cedarschema_str( " entity User = { name: String, @@ -1154,7 +1154,7 @@ when { .expect("should succeed"); pset.add(policy.into()).expect("should succeed"); - let schema = ValidatorSchema::from_str_natural( + let schema = ValidatorSchema::from_cedarschema_str( " entity User = { name: String, From 3c19c9907a68e566b4f8670904040404714dbb3c Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 27 Aug 2024 11:13:27 -0600 Subject: [PATCH 40/56] address feedback from @john-h-kastner-aws Signed-off-by: oflatt --- cedar-policy-core/src/ast/request.rs | 4 +- cedar-policy-validator/src/entity_manifest.rs | 711 +++++++++--------- .../src/types/request_env.rs | 2 +- cedar-policy/src/api/id.rs | 2 +- 4 files changed, 371 insertions(+), 348 deletions(-) diff --git a/cedar-policy-core/src/ast/request.rs b/cedar-policy-core/src/ast/request.rs index def22fcbb0..dda97e21c8 100644 --- a/cedar-policy-core/src/ast/request.rs +++ b/cedar-policy-core/src/ast/request.rs @@ -205,8 +205,8 @@ impl Request { /// Returns `None` if the request is not fully concrete. pub fn to_request_type(&self) -> Option { Some(RequestType { - principal: self.principal.uid()?.entity_type().clone(), - action: self.action.uid()?.clone(), + principal: self.principal().uid()?.entity_type().clone(), + action: self.action().uid()?.clone(), resource: self.resource().uid()?.entity_type().clone(), }) } diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index 40451a0034..a5391dfa5a 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -58,7 +58,7 @@ where /// A map from request types to [`RootAccessTrie`]s. #[serde_as(as = "Vec<(_, _)>")] #[serde(bound(deserialize = "T: Default"))] - pub per_action: HashMap>, + per_action: HashMap>, } /// A map of data fields to [`AccessTrie`]s. @@ -114,7 +114,7 @@ where /// The data that needs to be loaded, organized by root. #[serde_as(as = "Vec<(_, _)>")] #[serde(bound(deserialize = "T: Default"))] - pub trie: HashMap>, + trie: HashMap>, } /// A Trie representing a set of data paths to load, @@ -133,15 +133,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<(_, _)>")] - pub children: Fields, + children: Fields, /// For entity types, this boolean may be `true` /// to signal that all the ancestors in the entity hierarchy /// are required (transitively). - pub ancestors_required: bool, + ancestors_required: bool, /// Optional data annotation, usually used for type information. #[serde(skip_serializing, skip_deserializing)] #[serde(bound(deserialize = "T: Default"))] - pub data: T, + data: T, } /// A data path that may end with requesting the parents of @@ -175,7 +175,7 @@ struct AccessPath { /// This error message tells the user not to use certain operators /// before accessing record or entity attributes, breaking this grammar. #[derive(Debug, Clone, Error, Hash, Eq, PartialEq)] -#[error("For policy `{policy_id}`, failed to analyze expression while computing entity manifest.`")] +#[error("for policy `{policy_id}`, failed to analyze expression while computing entity manifest`")] pub struct FailedAnalysisError { /// Source location pub source_loc: Option, @@ -225,20 +225,20 @@ pub enum EntityManifestError { /// A validation error was encountered #[error(transparent)] #[diagnostic(transparent)] - ValidationError(#[from] ValidationError), + Validation(#[from] ValidationError), /// A entities error was encountered #[error(transparent)] #[diagnostic(transparent)] - EntitiesError(#[from] EntitiesError), + Entities(#[from] EntitiesError), /// The request was partial #[error(transparent)] #[diagnostic(transparent)] - PartialRequestError(#[from] PartialRequestError), + PartialRequest(#[from] PartialRequestError), /// A policy was partial #[error(transparent)] #[diagnostic(transparent)] - PartialExpressionError(#[from] PartialExpressionError), + PartialExpression(#[from] PartialExpressionError), /// A policy was not analyzable because it used unsupported operators /// before a [`ExprKind::GetAttr`] @@ -248,6 +248,12 @@ pub enum EntityManifestError { FailedAnalysis(#[from] FailedAnalysisError), } +impl EntityManifest { + pub fn per_action(&self) -> &HashMap> { + &self.per_action + } +} + /// Union two tries by combining the fields. fn union_fields(first: &Fields, second: &Fields) -> Fields { let mut res = first.clone(); @@ -290,6 +296,12 @@ impl AccessPath { } } +impl RootAccessTrie { + pub fn trie(&self) -> &HashMap> { + &self.trie + } +} + impl RootAccessTrie { /// Create an empty [`RootAccessTrie`] that requests nothing. pub fn new() -> Self { @@ -330,6 +342,18 @@ impl AccessTrie { data: self.data.clone(), } } + + pub fn children(&self) -> &Fields { + &self.children + } + + pub fn ancestors_required(&self) -> bool { + self.ancestors_required + } + + pub fn data(&self) -> &T { + &self.data + } } impl AccessTrie { @@ -678,43 +702,42 @@ when { let schema = schema(); let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); - let expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "Read" - }, - "resource": "Document" - }, - { - "trie": [ - [ - { - "Var": "principal" - }, - { - "children": [ - [ - "name", - { - "children": [], - "ancestors_required": false - } + let expected = serde_json::json! ({ + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "name", + { + "children": [], + "ancestors_required": false + } + ] + ], + "ancestors_required": false + } + ] ] - ], - "ancestors_required": false - } + } + ] ] - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).unwrap(); + }); + let expected_manifest = serde_json::from_value(expected).unwrap(); assert_eq!(entity_manifest, expected_manifest); } @@ -728,26 +751,26 @@ when { let schema = schema(); let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); - let expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "Read" - }, - "resource": "Document" - }, - { - "trie": [ - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).unwrap(); + let expected = serde_json::json!( + { + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + ] + } + ] + ] + }); + let expected_manifest = serde_json::from_value(expected).unwrap(); assert_eq!(entity_manifest, expected_manifest); } @@ -782,43 +805,43 @@ action Read appliesTo { .0; let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); - let expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "Read" - }, - "resource": "Document" - }, - { - "trie": [ - [ - { - "Var": "principal" - }, - { - "children": [ - [ - "manager", - { - "children": [], - "ancestors_required": true - } + let expected = serde_json::json!( + { + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "manager", + { + "children": [], + "ancestors_required": true + } + ] + ], + "ancestors_required": true + } + ] ] - ], - "ancestors_required": true - } + } + ] ] - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).unwrap(); + }); + let expected_manifest = serde_json::from_value(expected).unwrap(); assert_eq!(entity_manifest, expected_manifest); } @@ -859,74 +882,74 @@ action Read appliesTo { .0; let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); - let expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "Read" - }, - "resource": "Document" - }, - { - "trie": [ - [ - { - "Var": "principal" - }, - { - "children": [ - [ - "name", - { - "children": [], - "ancestors_required": false - } + let expected = serde_json::json!( + { + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "name", + { + "children": [], + "ancestors_required": false + } + ] + ], + "ancestors_required": false + } + ] ] - ], - "ancestors_required": false - } - ] - ] - } - ], - [ - { - "principal": "OtherUserType", - "action": { - "ty": "Action", - "eid": "Read" - }, - "resource": "Document" - }, - { - "trie": [ - [ - { - "Var": "principal" - }, - { - "children": [ - [ - "name", - { - "children": [], - "ancestors_required": false - } + } + ], + [ + { + "principal": "OtherUserType", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "name", + { + "children": [], + "ancestors_required": false + } + ] + ], + "ancestors_required": false + } + ] ] - ], - "ancestors_required": false - } + } + ] ] - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).unwrap(); + }); + let expected_manifest = serde_json::from_value(expected).unwrap(); assert_eq!(entity_manifest, expected_manifest); } @@ -988,58 +1011,58 @@ action Read appliesTo { .0; let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); - let expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "Read" - }, - "resource": "Document" - }, - { - "trie": [ - [ - { - "Var": "resource" - }, - { - "children": [ - [ - "metadata", - { - "children": [ - [ - "owner", - { - "children": [], - "ancestors_required": false - } - ] - ], - "ancestors_required": false - } - ], - [ - "readers", - { - "children": [], - "ancestors_required": false - } + let expected = serde_json::json!( + { + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Read" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "resource" + }, + { + "children": [ + [ + "metadata", + { + "children": [ + [ + "owner", + { + "children": [], + "ancestors_required": false + } + ] + ], + "ancestors_required": false + } + ], + [ + "readers", + { + "children": [], + "ancestors_required": false + } + ] + ], + "ancestors_required": false + } + ] ] - ], - "ancestors_required": false - } + } + ] ] - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).unwrap(); + }); + let expected_manifest = serde_json::from_value(expected).unwrap(); assert_eq!(entity_manifest, expected_manifest); } @@ -1084,58 +1107,58 @@ action BeSad appliesTo { .0; let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); - let expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "BeSad" - }, - "resource": "Document" - }, - { - "trie": [ - [ - { - "Var": "principal" - }, - { - "children": [ - [ - "metadata", - { - "children": [ - [ - "nickname", - { - "children": [], - "ancestors_required": false - } + let expected = serde_json::json!( + { + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "BeSad" + }, + "resource": "Document" + }, + { + "trie": [ + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "metadata", + { + "children": [ + [ + "nickname", + { + "children": [], + "ancestors_required": false + } + ], + [ + "friends", + { + "children": [], + "ancestors_required": false + } + ] + ], + "ancestors_required": false + } + ] ], - [ - "friends", - { - "children": [], - "ancestors_required": false - } - ] - ], - "ancestors_required": false - } + "ancestors_required": false + } + ] ] - ], - "ancestors_required": false - } + } + ] ] - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).unwrap(); + }); + let expected_manifest = serde_json::from_value(expected).unwrap(); assert_eq!(entity_manifest, expected_manifest); } @@ -1177,90 +1200,90 @@ action Hello appliesTo { .0; let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); - let expected = r#" -{ - "per_action": [ - [ - { - "principal": "User", - "action": { - "ty": "Action", - "eid": "Hello" - }, - "resource": "User" - }, - { - "trie": [ - [ - { - "Var": "resource" - }, - { - "children": [ - [ - "metadata", - { - "children": [ - [ - "friends", - { - "children": [], - "ancestors_required": false - } + let expected = serde_json::json!( + { + "per_action": [ + [ + { + "principal": "User", + "action": { + "ty": "Action", + "eid": "Hello" + }, + "resource": "User" + }, + { + "trie": [ + [ + { + "Var": "resource" + }, + { + "children": [ + [ + "metadata", + { + "children": [ + [ + "friends", + { + "children": [], + "ancestors_required": false + } + ], + [ + "nickname", + { + "children": [], + "ancestors_required": false + } + ] + ], + "ancestors_required": false + } + ] ], - [ - "nickname", - { - "children": [], - "ancestors_required": false - } - ] - ], - "ancestors_required": false - } - ] - ], - "ancestors_required": false - } - ], - [ - { - "Var": "principal" - }, - { - "children": [ - [ - "metadata", - { - "children": [ - [ - "nickname", - { - "children": [], - "ancestors_required": false - } + "ancestors_required": false + } + ], + [ + { + "Var": "principal" + }, + { + "children": [ + [ + "metadata", + { + "children": [ + [ + "nickname", + { + "children": [], + "ancestors_required": false + } + ], + [ + "friends", + { + "children": [], + "ancestors_required": false + } + ] + ], + "ancestors_required": false + } + ] ], - [ - "friends", - { - "children": [], - "ancestors_required": false - } - ] - ], - "ancestors_required": false - } + "ancestors_required": false + } + ] ] - ], - "ancestors_required": false - } + } + ] ] - ] - } - ] - ] -}"#; - let expected_manifest = serde_json::from_str(expected).unwrap(); + }); + let expected_manifest = serde_json::from_value(expected).unwrap(); assert_eq!(entity_manifest, expected_manifest); } } diff --git a/cedar-policy-validator/src/types/request_env.rs b/cedar-policy-validator/src/types/request_env.rs index ab4f85c7b9..00ac406229 100644 --- a/cedar-policy-validator/src/types/request_env.rs +++ b/cedar-policy-validator/src/types/request_env.rs @@ -23,7 +23,7 @@ use super::Type; /// Represents a request type environment. In principle, this contains full /// types for the four variables (principal, action, resource, context). -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)] +#[derive(Clone, Debug, PartialEq)] pub enum RequestEnv<'a> { /// Contains the four variables bound in the type environment. These together /// represent the full type of (principal, action, resource, context) diff --git a/cedar-policy/src/api/id.rs b/cedar-policy/src/api/id.rs index 051fe6c1a7..2b3fbf2f61 100644 --- a/cedar-policy/src/api/id.rs +++ b/cedar-policy/src/api/id.rs @@ -1,6 +1,6 @@ /* * Copyright Cedar Contributors - * +y * * 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 From 4f05181bcc4422c6272cd8ca2eebe7d2b7ec8adb Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 27 Aug 2024 11:25:02 -0600 Subject: [PATCH 41/56] docs and camel case Signed-off-by: oflatt --- cedar-policy-core/src/ast/request.rs | 1 + cedar-policy-validator/src/entity_manifest.rs | 91 +++++++++++-------- .../src/types/request_env.rs | 1 - 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/cedar-policy-core/src/ast/request.rs b/cedar-policy-core/src/ast/request.rs index dda97e21c8..2e0092c0cb 100644 --- a/cedar-policy-core/src/ast/request.rs +++ b/cedar-policy-core/src/ast/request.rs @@ -51,6 +51,7 @@ pub struct Request { /// Represents the principal type, resource type, and action UID. #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct RequestType { /// Principal type pub principal: EntityType, diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index a5391dfa5a..1b61086e46 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -51,6 +51,7 @@ use crate::{ #[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct EntityManifest where T: Clone, @@ -76,6 +77,7 @@ pub type Fields = HashMap>>; // when adding public methods. #[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] pub enum EntityRoot { /// Literal entity ids Literal(EntityUID), @@ -107,6 +109,7 @@ impl Display for EntityRoot { #[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct RootAccessTrie where T: Clone, @@ -129,6 +132,7 @@ where #[doc = include_str!("../../cedar-policy/experimental_warning.md")] #[serde_as] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct AccessTrie { /// Child data of this entity slice. /// The keys are edges in the trie pointing to sub-trie values. @@ -249,6 +253,8 @@ pub enum EntityManifestError { } impl EntityManifest { + /// Get the contents of the entity manifest + /// indexed by the type of the request. pub fn per_action(&self) -> &HashMap> { &self.per_action } @@ -297,6 +303,8 @@ impl AccessPath { } impl RootAccessTrie { + /// Get the trie as a hash map from [`EntityRoot`] + /// to sub-[`AccessTrie`]s. pub fn trie(&self) -> &HashMap> { &self.trie } @@ -343,14 +351,19 @@ impl AccessTrie { } } + /// Get the children of this [`AccessTrie`]. pub fn children(&self) -> &Fields { &self.children } + /// Get a boolean which is true if this trie + /// requires all ancestors of the entity to be loaded. pub fn ancestors_required(&self) -> bool { self.ancestors_required } + /// Get the data associated with this [`AccessTrie`]. + /// This is usually `()` unless it is annotated by a type. pub fn data(&self) -> &T { &self.data } @@ -703,7 +716,7 @@ when { let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = serde_json::json! ({ - "per_action": [ + "perAction": [ [ { "principal": "User", @@ -717,7 +730,7 @@ when { "trie": [ [ { - "Var": "principal" + "var": "principal" }, { "children": [ @@ -725,11 +738,11 @@ when { "name", { "children": [], - "ancestors_required": false + "ancestorsRequired": false } ] ], - "ancestors_required": false + "ancestorsRequired": false } ] ] @@ -753,7 +766,7 @@ when { let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = serde_json::json!( { - "per_action": [ + "perAction": [ [ { "principal": "User", @@ -807,7 +820,7 @@ action Read appliesTo { let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = serde_json::json!( { - "per_action": [ + "perAction": [ [ { "principal": "User", @@ -821,7 +834,7 @@ action Read appliesTo { "trie": [ [ { - "Var": "principal" + "var": "principal" }, { "children": [ @@ -829,11 +842,11 @@ action Read appliesTo { "manager", { "children": [], - "ancestors_required": true + "ancestorsRequired": true } ] ], - "ancestors_required": true + "ancestorsRequired": true } ] ] @@ -884,7 +897,7 @@ action Read appliesTo { let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = serde_json::json!( { - "per_action": [ + "perAction": [ [ { "principal": "User", @@ -898,7 +911,7 @@ action Read appliesTo { "trie": [ [ { - "Var": "principal" + "var": "principal" }, { "children": [ @@ -906,11 +919,11 @@ action Read appliesTo { "name", { "children": [], - "ancestors_required": false + "ancestorsRequired": false } ] ], - "ancestors_required": false + "ancestorsRequired": false } ] ] @@ -929,7 +942,7 @@ action Read appliesTo { "trie": [ [ { - "Var": "principal" + "var": "principal" }, { "children": [ @@ -937,11 +950,11 @@ action Read appliesTo { "name", { "children": [], - "ancestors_required": false + "ancestorsRequired": false } ] ], - "ancestors_required": false + "ancestorsRequired": false } ] ] @@ -1013,7 +1026,7 @@ action Read appliesTo { let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = serde_json::json!( { - "per_action": [ + "perAction": [ [ { "principal": "User", @@ -1027,7 +1040,7 @@ action Read appliesTo { "trie": [ [ { - "Var": "resource" + "var": "resource" }, { "children": [ @@ -1039,22 +1052,22 @@ action Read appliesTo { "owner", { "children": [], - "ancestors_required": false + "ancestorsRequired": false } ] ], - "ancestors_required": false + "ancestorsRequired": false } ], [ "readers", { "children": [], - "ancestors_required": false + "ancestorsRequired": false } ] ], - "ancestors_required": false + "ancestorsRequired": false } ] ] @@ -1109,7 +1122,7 @@ action BeSad appliesTo { let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = serde_json::json!( { - "per_action": [ + "perAction": [ [ { "principal": "User", @@ -1123,7 +1136,7 @@ action BeSad appliesTo { "trie": [ [ { - "Var": "principal" + "var": "principal" }, { "children": [ @@ -1135,22 +1148,22 @@ action BeSad appliesTo { "nickname", { "children": [], - "ancestors_required": false + "ancestorsRequired": false } ], [ "friends", { "children": [], - "ancestors_required": false + "ancestorsRequired": false } ] ], - "ancestors_required": false + "ancestorsRequired": false } ] ], - "ancestors_required": false + "ancestorsRequired": false } ] ] @@ -1202,7 +1215,7 @@ action Hello appliesTo { let entity_manifest = compute_entity_manifest(&schema, &pset).expect("Should succeed"); let expected = serde_json::json!( { - "per_action": [ + "perAction": [ [ { "principal": "User", @@ -1216,7 +1229,7 @@ action Hello appliesTo { "trie": [ [ { - "Var": "resource" + "var": "resource" }, { "children": [ @@ -1228,27 +1241,27 @@ action Hello appliesTo { "friends", { "children": [], - "ancestors_required": false + "ancestorsRequired": false } ], [ "nickname", { "children": [], - "ancestors_required": false + "ancestorsRequired": false } ] ], - "ancestors_required": false + "ancestorsRequired": false } ] ], - "ancestors_required": false + "ancestorsRequired": false } ], [ { - "Var": "principal" + "var": "principal" }, { "children": [ @@ -1260,22 +1273,22 @@ action Hello appliesTo { "nickname", { "children": [], - "ancestors_required": false + "ancestorsRequired": false } ], [ "friends", { "children": [], - "ancestors_required": false + "ancestorsRequired": false } ] ], - "ancestors_required": false + "ancestorsRequired": false } ] ], - "ancestors_required": false + "ancestorsRequired": false } ] ] diff --git a/cedar-policy-validator/src/types/request_env.rs b/cedar-policy-validator/src/types/request_env.rs index 00ac406229..5dc2eb14c5 100644 --- a/cedar-policy-validator/src/types/request_env.rs +++ b/cedar-policy-validator/src/types/request_env.rs @@ -15,7 +15,6 @@ */ use cedar_policy_core::ast::{EntityType, EntityUID, RequestType}; -use serde::Serialize; use crate::ValidatorSchema; From e81929ac764cadfa736fc2abbd6fb5785252ad33 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 27 Aug 2024 11:36:06 -0600 Subject: [PATCH 42/56] add todo Signed-off-by: oflatt --- cedar-policy/src/api.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 4a643e3804..ed05a394a2 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -25,6 +25,7 @@ mod id; #[cfg(feature = "entity-manifest")] use cedar_policy_validator::entity_manifest; +// TODO implement wrappers for these structs before they become public #[cfg(feature = "entity-manifest")] pub use cedar_policy_validator::entity_manifest::{ AccessTrie, EntityManifest, EntityRoot, Fields, RootAccessTrie, From 50d604bd0c76e35f38a4041ba3a01dd097dc90e4 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 27 Aug 2024 11:53:46 -0600 Subject: [PATCH 43/56] use validationresult Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index 1b61086e46..40bf71856a 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -31,12 +31,12 @@ use serde_with::serde_as; use smol_str::SmolStr; use thiserror::Error; -use crate::ValidationError; use crate::{ typecheck::{PolicyCheck, Typechecker}, types::{EntityRecordKind, Type}, ValidationMode, ValidatorSchema, }; +use crate::{ValidationError, ValidationResult}; /// Data structure storing what data is needed /// based on the the [`RequestType`]. @@ -229,7 +229,7 @@ pub enum EntityManifestError { /// A validation error was encountered #[error(transparent)] #[diagnostic(transparent)] - Validation(#[from] ValidationError), + Validation(#[from] ValidationResult), /// A entities error was encountered #[error(transparent)] #[diagnostic(transparent)] @@ -406,15 +406,7 @@ pub fn compute_entity_manifest( Ok(RootAccessTrie::new()) } - PolicyCheck::Fail(errors) => { - // PANIC SAFETY: policy check fail should be a non-empty vector. - #[allow(clippy::expect_used)] - Err(errors - .first() - .expect("Policy check failed without an error") - .clone() - .into()) - } + PolicyCheck::Fail(errors) => Err(ValidationResult::new(errors, vec![])), }?; let request_type = request_env From 5f95c347c31dcbf0310062ffefb635b34c98a098 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 27 Aug 2024 12:21:03 -0600 Subject: [PATCH 44/56] Revert "use validationresult" This reverts commit 65c13248f15205129b6cb7e0dcd5b5fff5d44ede. Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index 40bf71856a..1b61086e46 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -31,12 +31,12 @@ use serde_with::serde_as; use smol_str::SmolStr; use thiserror::Error; +use crate::ValidationError; use crate::{ typecheck::{PolicyCheck, Typechecker}, types::{EntityRecordKind, Type}, ValidationMode, ValidatorSchema, }; -use crate::{ValidationError, ValidationResult}; /// Data structure storing what data is needed /// based on the the [`RequestType`]. @@ -229,7 +229,7 @@ pub enum EntityManifestError { /// A validation error was encountered #[error(transparent)] #[diagnostic(transparent)] - Validation(#[from] ValidationResult), + Validation(#[from] ValidationError), /// A entities error was encountered #[error(transparent)] #[diagnostic(transparent)] @@ -406,7 +406,15 @@ pub fn compute_entity_manifest( Ok(RootAccessTrie::new()) } - PolicyCheck::Fail(errors) => Err(ValidationResult::new(errors, vec![])), + PolicyCheck::Fail(errors) => { + // PANIC SAFETY: policy check fail should be a non-empty vector. + #[allow(clippy::expect_used)] + Err(errors + .first() + .expect("Policy check failed without an error") + .clone() + .into()) + } }?; let request_type = request_env From 2a3e365eb6b707373a600c93a4c4f352a79518d1 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 27 Aug 2024 12:42:10 -0600 Subject: [PATCH 45/56] working on error type Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 18 ++++-- cedar-policy/src/api.rs | 2 +- cedar-policy/src/api/err.rs | 63 +++++++++++++++++-- 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index 1b61086e46..1cb478f9a1 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -178,15 +178,18 @@ struct AccessPath { /// The `get_expr_path` function handles `datapath-expr` expressions. /// This error message tells the user not to use certain operators /// before accessing record or entity attributes, breaking this grammar. +// 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, Hash, Eq, PartialEq)] #[error("for policy `{policy_id}`, failed to analyze expression while computing entity manifest`")] pub struct FailedAnalysisError { /// Source location - pub source_loc: Option, + source_loc: Option, /// Policy ID where the error occurred - pub policy_id: PolicyID, + policy_id: PolicyID, /// The kind of the expression that was unexpected - pub expr_kind: ExprKind>, + expr_kind: ExprKind>, } impl Diagnostic for FailedAnalysisError { @@ -202,6 +205,9 @@ impl Diagnostic for FailedAnalysisError { /// Error when expressions 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, Hash, Eq, PartialEq)] #[error( "Entity slicing requires fully concrete policies. Got a policy with an unknown expression." @@ -212,6 +218,9 @@ impl Diagnostic for PartialExpressionError {} /// Error when the request is 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, Hash, Eq, PartialEq)] #[error("Entity slicing requires a fully concrete request. Got a partial request.")] pub struct PartialRequestError {} @@ -220,9 +229,6 @@ impl Diagnostic for PartialRequestError {} /// An error generated by entity slicing. /// See [`FailedAnalysisError`] for details on the fragment /// of Cedar handled by 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, Error, Diagnostic)] #[non_exhaustive] pub enum EntityManifestError { diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index ed05a394a2..96e55e121f 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -4290,5 +4290,5 @@ pub fn compute_entity_manifest( schema: &Schema, pset: &PolicySet, ) -> Result { - entity_manifest::compute_entity_manifest(&schema.0, &pset.ast) + entity_manifest::compute_entity_manifest(&schema.0, &pset.ast).into() } diff --git a/cedar-policy/src/api/err.rs b/cedar-policy/src/api/err.rs index e86b99855b..b458a7242b 100644 --- a/cedar-policy/src/api/err.rs +++ b/cedar-policy/src/api/err.rs @@ -21,12 +21,16 @@ pub use cedar_policy_core::ast::{ expression_construction_errors, restricted_expr_errors, ContainsUnknown, ExpressionConstructionError, PartialValueToValueError, RestrictedExpressionError, }; +use cedar_policy_core::entities::err::EntitiesError; pub use cedar_policy_core::evaluator::{evaluation_errors, EvaluationError}; pub use cedar_policy_core::extensions::{ extension_function_lookup_errors, ExtensionFunctionLookupError, }; use cedar_policy_core::{ast, authorizer, est}; pub use cedar_policy_validator::cedar_schema::{schema_warnings, SchemaWarning}; +use cedar_policy_validator::entity_manifest::{ + self, FailedAnalysisError, PartialExpressionError, PartialRequestError, +}; pub use cedar_policy_validator::{schema_errors, SchemaError}; use miette::Diagnostic; use ref_cast::RefCast; @@ -39,10 +43,6 @@ pub mod entities_errors { pub use cedar_policy_core::entities::err::{Duplicate, EntitiesError, TransitiveClosureError}; } -/// Entity manifest errors -#[cfg(feature = "entity-manifest")] -pub use cedar_policy_validator::entity_manifest::EntityManifestError; - /// Errors related to serializing/deserializing entities or contexts to/from JSON pub mod entities_json_errors { pub use cedar_policy_core::entities::json::err::{ @@ -1165,3 +1165,58 @@ pub mod request_validation_errors { #[diagnostic(transparent)] pub struct TypeOfContextError(#[from] cedar_policy_core::entities::json::GetSchemaTypeError); } + +/// An error generated by entity slicing. +/// See [`FailedAnalysisError`] for details on the fragment +/// of Cedar handled by 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, Error, Diagnostic)] +#[non_exhaustive] +#[cfg(feature = "entity-slicing")] +pub enum EntityManifestError { + /// A validation error was encountered + #[error(transparent)] + #[diagnostic(transparent)] + Validation(#[from] ValidationError), + /// A entities error was encountered + #[error(transparent)] + #[diagnostic(transparent)] + Entities(#[from] EntitiesError), + + /// The request was partial + #[error(transparent)] + #[diagnostic(transparent)] + PartialRequest(#[from] PartialRequestError), + /// A policy was partial + #[error(transparent)] + #[diagnostic(transparent)] + PartialExpression(#[from] PartialExpressionError), + + /// A policy was not analyzable because it used unsupported operators + /// before a [`ExprKind::GetAttr`] + /// See [`FailedAnalysisError`] for more details. + #[error(transparent)] + #[diagnostic(transparent)] + FailedAnalysis(#[from] FailedAnalysisError), +} + +#[cfg(feature = "entity-slicing")] +impl From for EntityManifestError { + fn from(e: entity_manifest::EntityManifestError) -> Self { + match e { + entity_manifest::EntityManifestError::Validation(e) => Self::Validation(e.into()), + entity_manifest::EntityManifestError::Entities(e) => Self::Entities(e.into()), + entity_manifest::EntityManifestError::PartialRequest(e) => { + Self::PartialRequest(e.into()) + } + entity_manifest::EntityManifestError::PartialExpression(e) => { + Self::PartialExpression(e.into()) + } + entity_manifest::EntityManifestError::FailedAnalysis(e) => { + Self::FailedAnalysis(e.into()) + } + } + } +} From 4862481b4dc064874c1063ab3b7e0673624802c3 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 27 Aug 2024 12:47:59 -0600 Subject: [PATCH 46/56] make error wrapper for entity manifest work Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 1 - cedar-policy/src/api.rs | 2 +- cedar-policy/src/api/err.rs | 12 ++++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index 1cb478f9a1..c1282ada08 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -230,7 +230,6 @@ impl Diagnostic for PartialRequestError {} /// See [`FailedAnalysisError`] for details on the fragment /// of Cedar handled by entity slicing. #[derive(Debug, Error, Diagnostic)] -#[non_exhaustive] pub enum EntityManifestError { /// A validation error was encountered #[error(transparent)] diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 96e55e121f..2a50ecccd7 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -4290,5 +4290,5 @@ pub fn compute_entity_manifest( schema: &Schema, pset: &PolicySet, ) -> Result { - entity_manifest::compute_entity_manifest(&schema.0, &pset.ast).into() + entity_manifest::compute_entity_manifest(&schema.0, &pset.ast).map_err(|e| e.into()) } diff --git a/cedar-policy/src/api/err.rs b/cedar-policy/src/api/err.rs index b458a7242b..7c2961494c 100644 --- a/cedar-policy/src/api/err.rs +++ b/cedar-policy/src/api/err.rs @@ -1174,7 +1174,7 @@ pub mod request_validation_errors { // when adding public methods. #[derive(Debug, Error, Diagnostic)] #[non_exhaustive] -#[cfg(feature = "entity-slicing")] +#[cfg(feature = "entity-manifest")] pub enum EntityManifestError { /// A validation error was encountered #[error(transparent)] @@ -1202,20 +1202,20 @@ pub enum EntityManifestError { FailedAnalysis(#[from] FailedAnalysisError), } -#[cfg(feature = "entity-slicing")] +#[cfg(feature = "entity-manifest")] impl From for EntityManifestError { fn from(e: entity_manifest::EntityManifestError) -> Self { match e { entity_manifest::EntityManifestError::Validation(e) => Self::Validation(e.into()), - entity_manifest::EntityManifestError::Entities(e) => Self::Entities(e.into()), + entity_manifest::EntityManifestError::Entities(e) => Self::Entities(e), entity_manifest::EntityManifestError::PartialRequest(e) => { - Self::PartialRequest(e.into()) + Self::PartialRequest(e) } entity_manifest::EntityManifestError::PartialExpression(e) => { - Self::PartialExpression(e.into()) + Self::PartialExpression(e) } entity_manifest::EntityManifestError::FailedAnalysis(e) => { - Self::FailedAnalysis(e.into()) + Self::FailedAnalysis(e) } } } From a2afe8547ca835d5980b495c0ed8e7a81ddc5a4d Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 27 Aug 2024 13:04:23 -0600 Subject: [PATCH 47/56] finish making wrapper for entity manifest errors Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 33 +++++++++---------- cedar-policy/src/api.rs | 1 + cedar-policy/src/api/err.rs | 12 +++---- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index c1282ada08..6c9c893506 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -31,12 +31,12 @@ use serde_with::serde_as; use smol_str::SmolStr; use thiserror::Error; -use crate::ValidationError; use crate::{ typecheck::{PolicyCheck, Typechecker}, types::{EntityRecordKind, Type}, ValidationMode, ValidatorSchema, }; +use crate::{ValidationResult, Validator}; /// Data structure storing what data is needed /// based on the the [`RequestType`]. @@ -229,31 +229,27 @@ impl Diagnostic for PartialRequestError {} /// An error generated by entity slicing. /// See [`FailedAnalysisError`] for details on the fragment /// of Cedar handled by entity slicing. -#[derive(Debug, Error, Diagnostic)] +#[derive(Debug, Error)] pub enum EntityManifestError { /// A validation error was encountered - #[error(transparent)] - #[diagnostic(transparent)] - Validation(#[from] ValidationError), + // TODO impl Error for ValidationResult (it already is implemented for api::ValidationResult) + #[error("A validation error occurred.")] + Validation(ValidationResult), /// A entities error was encountered #[error(transparent)] - #[diagnostic(transparent)] Entities(#[from] EntitiesError), /// The request was partial #[error(transparent)] - #[diagnostic(transparent)] PartialRequest(#[from] PartialRequestError), /// A policy was partial #[error(transparent)] - #[diagnostic(transparent)] PartialExpression(#[from] PartialExpressionError), /// A policy was not analyzable because it used unsupported operators /// before a [`ExprKind::GetAttr`] /// See [`FailedAnalysisError`] for more details. #[error(transparent)] - #[diagnostic(transparent)] FailedAnalysis(#[from] FailedAnalysisError), } @@ -392,6 +388,13 @@ pub fn compute_entity_manifest( schema: &ValidatorSchema, policies: &PolicySet, ) -> Result { + // first, run strict validation to ensure there are no errors + let validator = Validator::new(schema.clone()); + let validation_res = validator.validate(policies, ValidationMode::Strict); + if !validation_res.validation_passed() { + return Err(EntityManifestError::Validation(validation_res)); + } + let mut manifest: HashMap = HashMap::new(); // now, for each policy we add the data it requires to the manifest @@ -411,14 +414,10 @@ pub fn compute_entity_manifest( Ok(RootAccessTrie::new()) } - PolicyCheck::Fail(errors) => { - // PANIC SAFETY: policy check fail should be a non-empty vector. - #[allow(clippy::expect_used)] - Err(errors - .first() - .expect("Policy check failed without an error") - .clone() - .into()) + // PANIC SAFETY: policy check should not fail after full strict validation above. + #[allow(clippy::panic)] + PolicyCheck::Fail(_errors) => { + panic!("Policy check failed after validation succeeded") } }?; diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 2a50ecccd7..ff524248f5 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -1619,6 +1619,7 @@ impl From for ValidationResult { } } + impl std::fmt::Display for ValidationResult { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.first_error_or_warning() { diff --git a/cedar-policy/src/api/err.rs b/cedar-policy/src/api/err.rs index 7c2961494c..f2f5e4594e 100644 --- a/cedar-policy/src/api/err.rs +++ b/cedar-policy/src/api/err.rs @@ -38,6 +38,8 @@ use smol_str::SmolStr; use thiserror::Error; use to_cedar_syntax_errors::NameCollisionsError; +use super::ValidationResult; + /// Errors related to [`crate::Entities`] pub mod entities_errors { pub use cedar_policy_core::entities::err::{Duplicate, EntitiesError, TransitiveClosureError}; @@ -1179,7 +1181,7 @@ pub enum EntityManifestError { /// A validation error was encountered #[error(transparent)] #[diagnostic(transparent)] - Validation(#[from] ValidationError), + Validation(#[from] ValidationResult), /// A entities error was encountered #[error(transparent)] #[diagnostic(transparent)] @@ -1208,15 +1210,11 @@ impl From for EntityManifestError { match e { entity_manifest::EntityManifestError::Validation(e) => Self::Validation(e.into()), entity_manifest::EntityManifestError::Entities(e) => Self::Entities(e), - entity_manifest::EntityManifestError::PartialRequest(e) => { - Self::PartialRequest(e) - } + entity_manifest::EntityManifestError::PartialRequest(e) => Self::PartialRequest(e), entity_manifest::EntityManifestError::PartialExpression(e) => { Self::PartialExpression(e) } - entity_manifest::EntityManifestError::FailedAnalysis(e) => { - Self::FailedAnalysis(e) - } + entity_manifest::EntityManifestError::FailedAnalysis(e) => Self::FailedAnalysis(e), } } } From ff06de6021562121ae8dba66003bded346292859 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 27 Aug 2024 13:11:43 -0600 Subject: [PATCH 48/56] fmt, docs Signed-off-by: oflatt --- cedar-policy/src/api.rs | 1 - cedar-policy/src/api/err.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index ff524248f5..2a50ecccd7 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -1619,7 +1619,6 @@ impl From for ValidationResult { } } - impl std::fmt::Display for ValidationResult { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.first_error_or_warning() { diff --git a/cedar-policy/src/api/err.rs b/cedar-policy/src/api/err.rs index f2f5e4594e..d24746cfb4 100644 --- a/cedar-policy/src/api/err.rs +++ b/cedar-policy/src/api/err.rs @@ -1196,8 +1196,7 @@ pub enum EntityManifestError { #[diagnostic(transparent)] PartialExpression(#[from] PartialExpressionError), - /// A policy was not analyzable because it used unsupported operators - /// before a [`ExprKind::GetAttr`] + /// A policy was not analyzable because it used unsupported operators. /// See [`FailedAnalysisError`] for more details. #[error(transparent)] #[diagnostic(transparent)] From 4bf079ed8cacb8b5173b4ad5dc2ea6dc2bf2dff9 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 27 Aug 2024 13:58:09 -0600 Subject: [PATCH 49/56] fix up non-feature build Signed-off-by: oflatt --- cedar-policy/src/api/err.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cedar-policy/src/api/err.rs b/cedar-policy/src/api/err.rs index d24746cfb4..42f7571af7 100644 --- a/cedar-policy/src/api/err.rs +++ b/cedar-policy/src/api/err.rs @@ -21,6 +21,7 @@ pub use cedar_policy_core::ast::{ expression_construction_errors, restricted_expr_errors, ContainsUnknown, ExpressionConstructionError, PartialValueToValueError, RestrictedExpressionError, }; +#[cfg(feature = "entity-manifest")] use cedar_policy_core::entities::err::EntitiesError; pub use cedar_policy_core::evaluator::{evaluation_errors, EvaluationError}; pub use cedar_policy_core::extensions::{ @@ -28,6 +29,7 @@ pub use cedar_policy_core::extensions::{ }; use cedar_policy_core::{ast, authorizer, est}; pub use cedar_policy_validator::cedar_schema::{schema_warnings, SchemaWarning}; +#[cfg(feature = "entity-manifest")] use cedar_policy_validator::entity_manifest::{ self, FailedAnalysisError, PartialExpressionError, PartialRequestError, }; @@ -38,6 +40,7 @@ use smol_str::SmolStr; use thiserror::Error; use to_cedar_syntax_errors::NameCollisionsError; +#[cfg(feature = "entity-manifest")] use super::ValidationResult; /// Errors related to [`crate::Entities`] From 9ac723b4762ba596f06fbb5fd44ddd084af00a02 Mon Sep 17 00:00:00 2001 From: oflatt Date: Tue, 27 Aug 2024 20:43:33 -0600 Subject: [PATCH 50/56] oops Signed-off-by: oflatt --- cedar-policy/src/api/id.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cedar-policy/src/api/id.rs b/cedar-policy/src/api/id.rs index 2b3fbf2f61..051fe6c1a7 100644 --- a/cedar-policy/src/api/id.rs +++ b/cedar-policy/src/api/id.rs @@ -1,6 +1,6 @@ /* * Copyright Cedar Contributors -y * + * * 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 From ef363e4d71f6f21fe64cd436cf3cfa569546ee53 Mon Sep 17 00:00:00 2001 From: Oliver Flatt Date: Thu, 29 Aug 2024 13:42:18 -0700 Subject: [PATCH 51/56] Update cedar-policy/CHANGELOG.md Co-authored-by: Kesha Hietala Signed-off-by: oflatt --- cedar-policy/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cedar-policy/CHANGELOG.md b/cedar-policy/CHANGELOG.md index 988f973b73..6e7db97df4 100644 --- a/cedar-policy/CHANGELOG.md +++ b/cedar-policy/CHANGELOG.md @@ -16,7 +16,7 @@ Cedar Language Version: 4.0 - Implemented [RFC 74](https://github.com/cedar-policy/rfcs/pull/74): A new experimental API (`compute_entity_manifest`) that provides the Entity Manifest: a data structure that describes what data is required to satisfy a - Cedar request. + Cedar request. To use this API you must enable the `entity-manifest` feature flag. - Additional functionality to the JSON FFI including parsing utilities (#1079) and conversion between the Cedar and JSON formats (#1087) - (*) Schema JSON syntax now accepts a type `EntityOrCommon` representing a From e0cdb8294cee343e00e3bf8a0add86b0bda0ae5d Mon Sep 17 00:00:00 2001 From: Oliver Flatt Date: Thu, 29 Aug 2024 13:42:58 -0700 Subject: [PATCH 52/56] Update cedar-policy-validator/src/entity_manifest.rs Co-authored-by: Kesha Hietala Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index 6c9c893506..3f8b776574 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -210,7 +210,7 @@ impl Diagnostic for FailedAnalysisError { // when adding public methods. #[derive(Debug, Clone, Error, Hash, Eq, PartialEq)] #[error( - "Entity slicing requires fully concrete policies. Got a policy with an unknown expression." + "entity slicing requires fully concrete policies. Got a policy with an unknown expression" )] pub struct PartialExpressionError {} From fc5639accc7120f57ce33ff3990faf1f24836045 Mon Sep 17 00:00:00 2001 From: Oliver Flatt Date: Thu, 29 Aug 2024 13:43:07 -0700 Subject: [PATCH 53/56] Update cedar-policy-validator/src/entity_manifest.rs Co-authored-by: Kesha Hietala Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index 3f8b776574..a21cbe5849 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -233,7 +233,7 @@ impl Diagnostic for PartialRequestError {} pub enum EntityManifestError { /// A validation error was encountered // TODO impl Error for ValidationResult (it already is implemented for api::ValidationResult) - #[error("A validation error occurred.")] + #[error("a validation error occurred")] Validation(ValidationResult), /// A entities error was encountered #[error(transparent)] From 5371391d6eeab8a3321a704c11d8d2cff7d1f523 Mon Sep 17 00:00:00 2001 From: Oliver Flatt Date: Thu, 29 Aug 2024 13:43:16 -0700 Subject: [PATCH 54/56] Update cedar-policy-validator/src/entity_manifest.rs Co-authored-by: Kesha Hietala Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index a21cbe5849..b3ec036d90 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -222,7 +222,7 @@ impl Diagnostic for PartialExpressionError {} // Don't make fields `pub`, don't make breaking changes, and use caution // when adding public methods. #[derive(Debug, Clone, Error, Hash, Eq, PartialEq)] -#[error("Entity slicing requires a fully concrete request. Got a partial request.")] +#[error("entity slicing requires a fully concrete request. Got a partial request")] pub struct PartialRequestError {} impl Diagnostic for PartialRequestError {} From 1db288b89a47160c8cff4d7e8dc2ac0174ab75cf Mon Sep 17 00:00:00 2001 From: oflatt Date: Thu, 29 Aug 2024 14:51:24 -0600 Subject: [PATCH 55/56] add issues for todos Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 6 ++---- cedar-policy/src/api.rs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index b3ec036d90..410e8f3d1d 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -209,9 +209,7 @@ impl Diagnostic for FailedAnalysisError { // Don't make fields `pub`, don't make breaking changes, and use caution // when adding public methods. #[derive(Debug, Clone, Error, Hash, Eq, PartialEq)] -#[error( - "entity slicing requires fully concrete policies. Got a policy with an unknown expression" -)] +#[error("entity slicing requires fully concrete policies. Got a policy with an unknown expression")] pub struct PartialExpressionError {} impl Diagnostic for PartialExpressionError {} @@ -232,7 +230,7 @@ impl Diagnostic for PartialRequestError {} #[derive(Debug, Error)] pub enum EntityManifestError { /// A validation error was encountered - // TODO impl Error for ValidationResult (it already is implemented for api::ValidationResult) + // TODO (#1158) impl Error for ValidationResult (it already is implemented for api::ValidationResult) #[error("a validation error occurred")] Validation(ValidationResult), /// A entities error was encountered diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 2a50ecccd7..aecc0f3ed6 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -25,7 +25,7 @@ mod id; #[cfg(feature = "entity-manifest")] use cedar_policy_validator::entity_manifest; -// TODO implement wrappers for these structs before they become public +// TODO (#1157) implement wrappers for these structs before they become public #[cfg(feature = "entity-manifest")] pub use cedar_policy_validator::entity_manifest::{ AccessTrie, EntityManifest, EntityRoot, Fields, RootAccessTrie, From ad96bd3f4e30e37b233de882e3d763a064d908f1 Mon Sep 17 00:00:00 2001 From: oflatt Date: Thu, 29 Aug 2024 14:52:03 -0600 Subject: [PATCH 56/56] copyright at the top Signed-off-by: oflatt --- cedar-policy-validator/src/entity_manifest.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index 410e8f3d1d..d726365554 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -1,5 +1,3 @@ -//! Entity Manifest definition and static analysis. - /* * Copyright Cedar Contributors * @@ -16,6 +14,8 @@ * limitations under the License. */ +//! Entity Manifest definition and static analysis. + use std::collections::HashMap; use std::fmt::{Display, Formatter};