Skip to content

Commit 91693c2

Browse files
Fix 1176 (#1177)
Signed-off-by: Andrew Wells <[email protected]>
1 parent 20e0195 commit 91693c2

File tree

3 files changed

+437
-27
lines changed

3 files changed

+437
-27
lines changed

cedar-policy-core/src/entities/conformance.rs

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
use std::collections::BTreeMap;
18+
1719
use super::{
1820
json::err::TypeMismatchError, schematype_of_restricted_expr, EntityTypeDescription,
1921
GetSchemaTypeError, HeterogeneousSetError, Schema, SchemaType,
@@ -24,6 +26,7 @@ use crate::ast::{
2426
use crate::extensions::{ExtensionFunctionLookupError, Extensions};
2527
use either::Either;
2628
use miette::Diagnostic;
29+
use smol_str::SmolStr;
2730
use thiserror::Error;
2831
pub mod err;
2932

@@ -176,6 +179,60 @@ pub fn typecheck_value_against_schematype(
176179
}
177180
}
178181

182+
/// Check whether the given `RestrictedExpr` is a valid instance of `SchemaType`
183+
pub fn does_restricted_expr_implement_schematype(
184+
expr: BorrowedRestrictedExpr<'_>,
185+
expected_ty: &SchemaType,
186+
) -> bool {
187+
use SchemaType::*;
188+
189+
match expected_ty {
190+
Bool => expr.as_bool().is_some(),
191+
Long => expr.as_long().is_some(),
192+
String => expr.as_string().is_some(),
193+
EmptySet => expr.as_set_elements().is_some_and(|e| e.count() == 0),
194+
Set { .. } if expr.as_set_elements().is_some_and(|e| e.count() == 0) => true,
195+
Set { element_ty: elty } => match expr.as_set_elements() {
196+
Some(mut els) => els.all(|e| does_restricted_expr_implement_schematype(e, elty)),
197+
None => false,
198+
},
199+
Record { attrs, open_attrs } => match expr.as_record_pairs() {
200+
Some(pairs) => {
201+
let pairs_map: BTreeMap<&SmolStr, BorrowedRestrictedExpr<'_>> = pairs.collect();
202+
let all_req_schema_attrs_in_record = attrs.iter().all(|(k, v)| {
203+
!v.required
204+
|| match pairs_map.get(k) {
205+
Some(inner_e) => {
206+
does_restricted_expr_implement_schematype(*inner_e, &v.attr_type)
207+
}
208+
None => false,
209+
}
210+
});
211+
let all_rec_attrs_match_schema =
212+
pairs_map.iter().all(|(k, inner_e)| match attrs.get(*k) {
213+
Some(sch_ty) => {
214+
does_restricted_expr_implement_schematype(*inner_e, &sch_ty.attr_type)
215+
}
216+
None => *open_attrs,
217+
});
218+
all_rec_attrs_match_schema && all_req_schema_attrs_in_record
219+
}
220+
None => false,
221+
},
222+
Extension { name } => match expr.as_extn_fn_call() {
223+
Some((actual_name, _)) => match name.0.id.as_ref() {
224+
"ipaddr" => actual_name.0.id.as_ref() == "ip",
225+
_ => name == actual_name,
226+
},
227+
None => false,
228+
},
229+
Entity { ty } => match expr.as_euid() {
230+
Some(actual_euid) => actual_euid.entity_type() == ty,
231+
None => false,
232+
},
233+
}
234+
}
235+
179236
/// Check whether the given `RestrictedExpr` typechecks with the given `SchemaType`.
180237
/// If the typecheck passes, return `Ok(())`.
181238
/// If the typecheck fails, return an appropriate `Err`.
@@ -184,23 +241,15 @@ pub fn typecheck_restricted_expr_against_schematype(
184241
expected_ty: &SchemaType,
185242
extensions: &Extensions<'_>,
186243
) -> Result<(), TypecheckError> {
187-
// TODO(#440): instead of computing the `SchemaType` of `expr` and then
188-
// checking whether the schematypes are "consistent", wouldn't it be less
189-
// confusing, more efficient, and maybe even more precise to just typecheck
190-
// directly?
244+
if does_restricted_expr_implement_schematype(expr, expected_ty) {
245+
return Ok(());
246+
}
191247
match schematype_of_restricted_expr(expr, extensions) {
192-
Ok(actual_ty) => {
193-
if actual_ty.is_consistent_with(expected_ty) {
194-
// typecheck passes
195-
Ok(())
196-
} else {
197-
Err(TypecheckError::TypeMismatch(TypeMismatchError {
198-
expected: Box::new(expected_ty.clone()),
199-
actual_ty: Some(Box::new(actual_ty)),
200-
actual_val: Either::Right(Box::new(expr.to_owned())),
201-
}))
202-
}
203-
}
248+
Ok(actual_ty) => Err(TypecheckError::TypeMismatch(TypeMismatchError {
249+
expected: Box::new(expected_ty.clone()),
250+
actual_ty: Some(Box::new(actual_ty)),
251+
actual_val: Either::Right(Box::new(expr.to_owned())),
252+
})),
204253
Err(GetSchemaTypeError::UnknownInsufficientTypeInfo { .. }) => {
205254
// in this case we just don't have the information to know whether
206255
// the attribute value (an unknown) matches the expected type.

cedar-policy-core/src/entities/json/schema_types.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ pub enum SchemaType {
6969
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
7070
pub struct AttributeType {
7171
/// Type of the attribute
72-
attr_type: SchemaType,
72+
pub(crate) attr_type: SchemaType,
7373
/// Is the attribute required
74-
required: bool,
74+
pub(crate) required: bool,
7575
}
7676

7777
impl SchemaType {
@@ -354,11 +354,8 @@ impl Diagnostic for NontrivialResidualError {
354354
/// required or optional.
355355
/// This function, when given a record that has keys A, B, and C, will return a
356356
/// `SchemaType` where A, B, and C are all marked as optional attributes, but no
357-
/// other attributes are possible.
358-
/// That is, this assumes that all existing attributes are optional, but that no
359-
/// other optional attributes are possible.
360-
/// Compared to marking A, B, and C as required, this allows the returned
361-
/// `SchemaType` to `is_consistent_with()` more types.
357+
/// other attributes are possible. This maximized flexibility while avoiding
358+
/// heterogeneous sets.
362359
///
363360
/// This function may return `GetSchemaTypeError`, but should never return
364361
/// `NontrivialResidual`, because `RestrictedExpr`s can't contain nontrivial
@@ -382,9 +379,8 @@ pub fn schematype_of_restricted_expr(
382379
BorrowedRestrictedExpr::new_unchecked(v), // assuming the invariant holds for the record as a whole, it will also hold for each attribute value
383380
extensions,
384381
)?;
385-
// we can't know if the attribute is required or optional,
386-
// but marking it optional is more flexible -- allows the
387-
// attribute type to `is_consistent_with()` more types
382+
// We can't know if the attribute is required or optional.
383+
// Keep as optional to minimize heterogeneous sets.
388384
Ok((k.clone(), AttributeType::optional(attr_type)))
389385
}).collect::<Result<BTreeMap<_,_>, GetSchemaTypeError>>()?,
390386
open_attrs: false,

0 commit comments

Comments
 (0)