diff --git a/cedar-language-server/src/schema/fold.rs b/cedar-language-server/src/schema/fold.rs index 29bdde9f59..671ab8fb5d 100644 --- a/cedar-language-server/src/schema/fold.rs +++ b/cedar-language-server/src/schema/fold.rs @@ -80,7 +80,7 @@ pub(crate) fn fold_schema(schema_info: &SchemaInfo) -> Option> .filter_map(|et| et.loc.as_loc_ref()); let action_locs = validator.action_ids().filter_map(|a| a.loc()); let common_types = validator - .common_types() + .common_types_extended() .filter_map(|ct| ct.type_loc.as_loc_ref()); // Combine all locations and create folding ranges diff --git a/cedar-language-server/src/schema/symbols.rs b/cedar-language-server/src/schema/symbols.rs index b0be0e6cc2..602f533746 100644 --- a/cedar-language-server/src/schema/symbols.rs +++ b/cedar-language-server/src/schema/symbols.rs @@ -125,7 +125,7 @@ pub(crate) fn schema_symbols(schema_info: &SchemaInfo) -> Option = validator - .common_types() + .common_types_extended() .filter_map(|ct| { ct.name_loc .as_ref() diff --git a/cedar-policy-core/src/ast.rs b/cedar-policy-core/src/ast.rs index e8367a647d..7b45cd8bc0 100644 --- a/cedar-policy-core/src/ast.rs +++ b/cedar-policy-core/src/ast.rs @@ -54,5 +54,7 @@ mod expr_iterator; pub use expr_iterator::*; mod annotation; pub use annotation::*; +mod generalized_slots_declaration; +pub use generalized_slots_declaration::*; mod expr_visitor; pub use expr_visitor::*; diff --git a/cedar-policy-core/src/ast/expr.rs b/cedar-policy-core/src/ast/expr.rs index ee3c567ed8..e81c763c2b 100644 --- a/cedar-policy-core/src/ast/expr.rs +++ b/cedar-policy-core/src/ast/expr.rs @@ -293,7 +293,7 @@ impl Expr { self.subexpressions() .filter_map(|exp| match &exp.expr_kind { ExprKind::Slot(slotid) => Some(Slot { - id: *slotid, + id: slotid.clone(), loc: exp.source_loc().into_maybe_loc(), }), _ => None, @@ -1842,7 +1842,7 @@ mod test { let e = Expr::slot(SlotId::principal()); let p = SlotId::principal(); let r = SlotId::resource(); - let set: HashSet = HashSet::from_iter([p]); + let set: HashSet = HashSet::from_iter([p.clone()]); assert_eq!(set, e.slots().map(|slot| slot.id).collect::>()); let e = Expr::or( Expr::slot(SlotId::principal()), diff --git a/cedar-policy-core/src/ast/expr_visitor.rs b/cedar-policy-core/src/ast/expr_visitor.rs index ee6acd3f26..c5adb9b353 100644 --- a/cedar-policy-core/src/ast/expr_visitor.rs +++ b/cedar-policy-core/src/ast/expr_visitor.rs @@ -55,7 +55,7 @@ pub trait ExprVisitor { match expr.expr_kind() { ExprKind::Lit(lit) => self.visit_literal(lit, loc), ExprKind::Var(var) => self.visit_var(*var, loc), - ExprKind::Slot(slot) => self.visit_slot(*slot, loc), + ExprKind::Slot(slot) => self.visit_slot(slot.clone(), loc), ExprKind::Unknown(unknown) => self.visit_unknown(unknown, loc), ExprKind::If { test_expr, diff --git a/cedar-policy-core/src/ast/generalized_slots_declaration.rs b/cedar-policy-core/src/ast/generalized_slots_declaration.rs new file mode 100644 index 0000000000..c1b02bd706 --- /dev/null +++ b/cedar-policy-core/src/ast/generalized_slots_declaration.rs @@ -0,0 +1,122 @@ +/* + * 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::BTreeMap; + +use crate::ast::SlotId; +use crate::extensions::Extensions; +use crate::validator::{ + json_schema::Type as JSONSchemaType, types::Type as ValidatorType, RawName, SchemaError, + ValidatorSchema, +}; +use serde::{Deserialize, Serialize}; + +/// Struct storing the pairs of SlotId's and their corresponding type +#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Hash, Serialize, Deserialize)] +pub struct GeneralizedSlotsDeclaration(BTreeMap>); + +impl GeneralizedSlotsDeclaration { + /// Create a new empty `GeneralizedSlotsDeclaration` (with no slots) + pub fn new() -> Self { + Self(BTreeMap::new()) + } + + /// Get the type of the slot by key + pub fn get(&self, key: &SlotId) -> Option<&JSONSchemaType> { + self.0.get(key) + } + + /// Iterate over all pairs of slots and their types + pub fn iter(&self) -> impl Iterator)> { + self.0.iter() + } + + /// Tell if it's empty + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Converts the types of generalized_slots_declaration to + /// use validator types so that they can be used by the typechecker + pub fn into_validator_generalized_slots_declaration( + self, + schema: &ValidatorSchema, + ) -> Result { + let validator_generalized_slots_declaration: Result, SchemaError> = self + .0 + .into_iter() + .map(|(k, ty)| -> Result<_, SchemaError> { + Ok(( + k, + schema.json_schema_type_to_validator_type(ty, Extensions::all_available())?, + )) + }) + .collect(); + Ok(validator_generalized_slots_declaration?.into()) + } +} + +impl Default for GeneralizedSlotsDeclaration { + fn default() -> Self { + Self::new() + } +} + +impl FromIterator<(SlotId, JSONSchemaType)> for GeneralizedSlotsDeclaration { + fn from_iter)>>(iter: T) -> Self { + Self(BTreeMap::from_iter(iter)) + } +} + +impl From>> for GeneralizedSlotsDeclaration { + fn from(value: BTreeMap>) -> Self { + Self(value) + } +} + +/// Struct storing the pairs of SlotId's and their corresponding validator types +#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Hash)] +pub struct ValidatorGeneralizedSlotsDeclaration(BTreeMap); + +impl FromIterator<(SlotId, ValidatorType)> for ValidatorGeneralizedSlotsDeclaration { + fn from_iter>(iter: T) -> Self { + Self(BTreeMap::from_iter(iter)) + } +} + +impl From> for ValidatorGeneralizedSlotsDeclaration { + fn from(value: BTreeMap) -> Self { + Self(value) + } +} + +impl Default for ValidatorGeneralizedSlotsDeclaration { + fn default() -> Self { + Self::new() + } +} + +impl ValidatorGeneralizedSlotsDeclaration { + /// Create a new empty `ValidatorGeneralizedSlotsDeclaration` (with no slots) + pub fn new() -> Self { + Self(BTreeMap::new()) + } + + /// Get the validator type of the slot by key + pub fn get(&self, slot: &SlotId) -> Option<&ValidatorType> { + self.0.get(slot) + } +} diff --git a/cedar-policy-core/src/ast/name.rs b/cedar-policy-core/src/ast/name.rs index 8482cf6e39..eb713995f9 100644 --- a/cedar-policy-core/src/ast/name.rs +++ b/cedar-policy-core/src/ast/name.rs @@ -283,7 +283,7 @@ impl<'de> Deserialize<'de> for InternalName { /// Clone is O(1). // This simply wraps a separate enum -- currently [`ValidSlotId`] -- in case we // want to generalize later -#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct SlotId(pub(crate) ValidSlotId); @@ -298,6 +298,11 @@ impl SlotId { Self(ValidSlotId::Resource) } + /// Create a `generalized slot` + pub fn generalized_slot(id: Id) -> Self { + Self(ValidSlotId::GeneralizedSlot(id)) + } + /// Check if a slot represents a principal pub fn is_principal(&self) -> bool { matches!(self, Self(ValidSlotId::Principal)) @@ -307,6 +312,11 @@ impl SlotId { pub fn is_resource(&self) -> bool { matches!(self, Self(ValidSlotId::Resource)) } + + /// Check if a slot represents a generalized slot + pub fn is_generalized_slot(&self) -> bool { + matches!(self, Self(ValidSlotId::GeneralizedSlot(_))) + } } impl From for SlotId { @@ -324,13 +334,14 @@ impl std::fmt::Display for SlotId { } } -/// Two possible variants for Slots -#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +/// Three possible variants for Slots +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub(crate) enum ValidSlotId { #[serde(rename = "?principal")] Principal, #[serde(rename = "?resource")] Resource, + GeneralizedSlot(Id), // Slots for generalized templates, for more info see [RFC 98](https://github.com/cedar-policy/rfcs/pull/98). } impl std::fmt::Display for ValidSlotId { @@ -338,6 +349,7 @@ impl std::fmt::Display for ValidSlotId { let s = match self { ValidSlotId::Principal => "principal", ValidSlotId::Resource => "resource", + ValidSlotId::GeneralizedSlot(id) => id.as_ref(), }; write!(f, "?{s}") } diff --git a/cedar-policy-core/src/ast/policy.rs b/cedar-policy-core/src/ast/policy.rs index 191d66079e..dedb7ff73a 100644 --- a/cedar-policy-core/src/ast/policy.rs +++ b/cedar-policy-core/src/ast/policy.rs @@ -15,9 +15,16 @@ */ use crate::ast::*; +use crate::entities::{conformance::typecheck_restricted_expr_against_schematype, SchemaType}; +use crate::extensions::Extensions; use crate::parser::{AsLocRef, IntoMaybeLoc, Loc, MaybeLoc}; +use crate::validator::{ + err::SchemaError, json_schema::Type as JSONSchemaType, types::Type as ValidatorType, RawName, + ValidatorSchema, +}; use annotation::{Annotation, Annotations}; use educe::Educe; +use generalized_slots_declaration::GeneralizedSlotsDeclaration; use itertools::Itertools; use miette::Diagnostic; use nonempty::{nonempty, NonEmpty}; @@ -53,6 +60,9 @@ cfg_tolerant_ast! { static DEFAULT_ANNOTATIONS: std::sync::LazyLock> = std::sync::LazyLock::new(|| Arc::new(Annotations::default())); + static DEFAULT_GENERALIZED_SLOTS_DECLARATION: std::sync::LazyLock> = + std::sync::LazyLock::new(|| Arc::new(GeneralizedSlotsDeclaration::default())); + static DEFAULT_PRINCIPAL_CONSTRAINT: std::sync::LazyLock = std::sync::LazyLock::new(PrincipalConstraint::any); @@ -120,6 +130,7 @@ impl Template { id: PolicyID, loc: MaybeLoc, annotations: Annotations, + generalized_slots_declaration: GeneralizedSlotsDeclaration, effect: Effect, principal_constraint: PrincipalConstraint, action_constraint: ActionConstraint, @@ -130,6 +141,7 @@ impl Template { id, loc, annotations, + generalized_slots_declaration, effect, principal_constraint, action_constraint, @@ -154,6 +166,7 @@ impl Template { id: PolicyID, loc: MaybeLoc, annotations: Arc, + generalized_slots_declaration: Arc, effect: Effect, principal_constraint: PrincipalConstraint, action_constraint: ActionConstraint, @@ -164,6 +177,7 @@ impl Template { id, loc, annotations, + generalized_slots_declaration, effect, principal_constraint, action_constraint, @@ -238,6 +252,18 @@ impl Template { self.body.annotations_arc() } + /// Get all generalized_slots_declaration data. + pub fn generalized_slots_declaration( + &self, + ) -> impl Iterator)> { + self.body.generalized_slots_declaration() + } + + /// Get [`Arc`] owning the generalized_slots_declaration data. + pub fn generalized_slots_declaration_arc(&self) -> &Arc { + self.body.generalized_slots_declaration_arc() + } + /// Get the condition expression of this template. /// /// This will be a conjunction of the template's scope constraints (on @@ -247,11 +273,18 @@ impl Template { self.body.condition() } - /// List of open slots in this template + /// List of open slots in this template including principal, resource, and generalized slots pub fn slots(&self) -> impl Iterator { self.slots.iter() } + /// List of principal and resource slots in this template + pub fn principal_resource_slots(&self) -> impl Iterator { + self.slots + .iter() + .filter(|slot| slot.id.is_principal() || slot.id.is_resource()) + } + /// Check if this template is a static policy /// /// Static policies can be linked without any slots, @@ -263,18 +296,40 @@ impl Template { /// Ensure that every slot in the template is bound by values, /// and that no extra values are bound in values /// This upholds invariant (values total map) + /// + /// All callers of this function + /// must enforce INVARIANT that `?principal` and `?resource` slots + /// are in values and generalized slots are in generalized_values pub fn check_binding( template: &Template, values: &HashMap, + generalized_values: &HashMap, ) -> Result<(), LinkingError> { // Verify all slots bound - let unbound = template + let unbound_values_and_generalized_values = template .slots .iter() - .filter(|slot| !values.contains_key(&slot.id)) + .filter(|slot| { + !values.contains_key(&slot.id) && !generalized_values.contains_key(&slot.id) + }) + .collect::>(); + + let extra_values = values + .iter() + .filter_map(|(slot, _)| { + if !template + .slots + .iter() + .any(|template_slot| template_slot.id == *slot) + { + Some(slot) + } else { + None + } + }) .collect::>(); - let extra = values + let extra_generalized_values = generalized_values .iter() .filter_map(|(slot, _)| { if !template @@ -289,16 +344,94 @@ impl Template { }) .collect::>(); - if unbound.is_empty() && extra.is_empty() { + if unbound_values_and_generalized_values.is_empty() + && extra_values.is_empty() + && extra_generalized_values.is_empty() + { Ok(()) } else { Err(LinkingError::from_unbound_and_extras( - unbound.into_iter().map(|slot| slot.id), - extra.into_iter().copied(), + unbound_values_and_generalized_values + .into_iter() + .map(|slot| slot.id.clone()), + extra_values + .into_iter() + .cloned() + .chain(extra_generalized_values.into_iter().cloned()), )) } } + /// Validates that the values provided for the generalized slots are of the types declared + pub fn link_time_type_checking_with_schema( + template: &Template, + schema: &ValidatorSchema, + values: &HashMap, + generalized_values: &HashMap, + ) -> Result<(), LinkingError> { + let validator_generalized_slots_declaration = GeneralizedSlotsDeclaration::from_iter( + template + .generalized_slots_declaration() + .map(|(k, v)| (k.clone(), v.clone())), + ) + .into_validator_generalized_slots_declaration(schema)?; + + let values_restricted_expr: HashMap = values + .iter() + .map(|(slot, entity_uid)| (slot.clone(), RestrictedExpr::val(entity_uid.clone()))) + .collect(); + + // we treat values differently because their type annotations are optional + for (slot, restricted_expr) in values_restricted_expr { + let maybe_validator_type = validator_generalized_slots_declaration.get(&slot); + if let Some(validator_type) = maybe_validator_type { + let borrowed_restricted_expr = restricted_expr.as_borrowed(); + #[allow(clippy::expect_used)] + let schema_ty = &SchemaType::try_from(validator_type.clone()).expect( + "This should never happen as expected_ty is a statically annotated type", + ); + let extensions = Extensions::all_available(); + typecheck_restricted_expr_against_schematype( + borrowed_restricted_expr, + schema_ty, + extensions, + ) + .map_err(|_| { + LinkingError::ValueProvidedForSlotIsNotOfTypeSpecified { + slot: slot.clone(), + value: restricted_expr.clone(), + ty: validator_type.clone(), + } + })? + } + } + + for (slot, restricted_expr) in generalized_values { + let validator_type = validator_generalized_slots_declaration.get(slot).ok_or( + LinkingError::ArityError { + unbound_values: vec![slot.clone()], + extra_values: vec![], + }, + )?; + let borrowed_restricted_expr = restricted_expr.as_borrowed(); + #[allow(clippy::expect_used)] + let schema_ty = &SchemaType::try_from(validator_type.clone()) + .expect("This should never happen as expected_ty is a statically annotated type"); + let extensions = Extensions::all_available(); + typecheck_restricted_expr_against_schematype( + borrowed_restricted_expr, + schema_ty, + extensions, + ) + .map_err(|_| LinkingError::ValueProvidedForSlotIsNotOfTypeSpecified { + slot: slot.clone(), + value: restricted_expr.clone(), + ty: validator_type.clone(), + })? + } + Ok(()) + } + /// Attempt to create a template-linked policy from this template. /// This will fail if values for all open slots are not given. /// `new_instance_id` is the `PolicyId` for the created template-linked policy. @@ -306,10 +439,24 @@ impl Template { template: Arc