Skip to content

Commit 2c401a4

Browse files
Fix typechecking ... in action (#462)
1 parent c7236c0 commit 2c401a4

File tree

4 files changed

+147
-63
lines changed

4 files changed

+147
-63
lines changed

cedar-policy-validator/src/rbac.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -383,12 +383,9 @@ impl Validator {
383383
.into_iter(),
384384
),
385385
// <var> in <literal euid>
386-
PrincipalOrResourceConstraint::In(EntityReference::EUID(euid)) => Box::new(
387-
self.schema
388-
.get_entity_types_in(euid.as_ref())
389-
.unwrap_or_default()
390-
.into_iter(),
391-
),
386+
PrincipalOrResourceConstraint::In(EntityReference::EUID(euid)) => {
387+
Box::new(self.schema.get_entity_types_in(euid.as_ref()).into_iter())
388+
}
392389
PrincipalOrResourceConstraint::Eq(EntityReference::Slot)
393390
| PrincipalOrResourceConstraint::In(EntityReference::Slot) => {
394391
Box::new(self.schema.known_entity_types())
@@ -406,7 +403,6 @@ impl Validator {
406403
Box::new(
407404
self.schema
408405
.get_entity_types_in(in_entity.as_ref())
409-
.unwrap_or_default()
410406
.into_iter()
411407
.filter(move |k| &entity_type == k),
412408
)

cedar-policy-validator/src/schema.rs

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,18 @@ impl ValidatorSchema {
471471

472472
/// Return true when the entity_type_id corresponds to a valid entity type.
473473
pub(crate) fn is_known_entity_type(&self, entity_type: &Name) -> bool {
474-
self.entity_types.contains_key(entity_type)
474+
is_action_entity_type(entity_type) || self.entity_types.contains_key(entity_type)
475+
}
476+
477+
/// Return true when `euid` has an entity type declared by the schema. We
478+
/// treat an Unspecified as "known" because it is always possible to declare
479+
/// an action using an unspecified principal/resource type without first
480+
/// declaring unspecified as an entity type in the entity types list.
481+
pub(crate) fn euid_has_known_entity_type(&self, euid: &EntityUID) -> bool {
482+
match euid.entity_type() {
483+
EntityType::Specified(ety) => self.is_known_entity_type(ety),
484+
EntityType::Unspecified => true,
485+
}
475486
}
476487

477488
/// An iterator over the action ids in the schema.
@@ -494,18 +505,18 @@ impl ValidatorSchema {
494505
/// includes all entity types that are descendants of the type of `entity`
495506
/// according to the schema, and the type of `entity` itself because
496507
/// `entity in entity` evaluates to `true`.
497-
pub(crate) fn get_entity_types_in<'a>(&'a self, entity: &'a EntityUID) -> Option<Vec<&Name>> {
498-
let ety = match entity.entity_type() {
499-
EntityType::Specified(ety) => Some(ety),
500-
EntityType::Unspecified => None,
501-
};
502-
503-
ety.and_then(|ety| self.get_entity_type(ety)).map(|ety| {
504-
ety.descendants
505-
.iter()
506-
.chain(std::iter::once(&ety.name))
507-
.collect()
508-
})
508+
pub(crate) fn get_entity_types_in<'a>(&'a self, entity: &'a EntityUID) -> Vec<&Name> {
509+
match entity.entity_type() {
510+
EntityType::Specified(ety) => {
511+
let mut descendants = self
512+
.get_entity_type(ety)
513+
.map(|v_ety| v_ety.descendants.iter().collect::<Vec<_>>())
514+
.unwrap_or_default();
515+
descendants.push(ety);
516+
descendants
517+
}
518+
EntityType::Unspecified => Vec::new(),
519+
}
509520
}
510521

511522
/// Get all entity types in the schema where an `{entity0} in {euids}` can
@@ -514,12 +525,11 @@ impl ValidatorSchema {
514525
pub(crate) fn get_entity_types_in_set<'a>(
515526
&'a self,
516527
euids: impl IntoIterator<Item = &'a EntityUID> + 'a,
517-
) -> Option<Vec<&Name>> {
528+
) -> impl Iterator<Item = &Name> {
518529
euids
519530
.into_iter()
520531
.map(|e| self.get_entity_types_in(e))
521-
.collect::<Option<Vec<_>>>()
522-
.map(|v| v.into_iter().flatten().collect::<Vec<_>>())
532+
.flatten()
523533
}
524534

525535
/// Get all action entities in the schema where `action in euids` evaluates

cedar-policy-validator/src/typecheck.rs

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,35 +1886,54 @@ impl<'a> Typechecker<'a> {
18861886
} else {
18871887
request_env.resource_entity_type()
18881888
};
1889-
let descendants = self.schema.get_entity_types_in_set(rhs.iter());
1890-
match (var_euid, descendants) {
1891-
// We failed to lookup the descendants because the entity type
1892-
// is not declared in the schema, or we failed to get the
1893-
// principal/resource entity type because the request is
1894-
// unknown. We don't know if the euid would be in the
1895-
// descendants or not, so give it type boolean.
1896-
(_, None) | (None, _) => {
1889+
match var_euid {
1890+
// We failed to get the principal/resource entity type because
1891+
// we are typechecking a request for some action which isn't
1892+
// declared in the schema. We don't know if the euid would be
1893+
// in the descendants or not, so give it type boolean.
1894+
None => {
18971895
let in_expr = ExprBuilder::with_data(Some(Type::primitive_boolean()))
18981896
.with_same_source_info(in_expr)
18991897
.is_in(lhs_expr, rhs_expr);
19001898
if self.mode.is_partial() {
19011899
TypecheckAnswer::success(in_expr)
19021900
} else {
1901+
// This should only happen when doing partial validation
1902+
// since we never construct the undeclared action
1903+
// request environment otherwise.
19031904
TypecheckAnswer::fail(in_expr)
19041905
}
19051906
}
1906-
(Some(EntityType::Specified(var_name)), Some(descendants)) => {
1907-
Typechecker::entity_in_descendants(
1908-
var_name,
1909-
descendants,
1910-
in_expr,
1911-
lhs_expr,
1912-
rhs_expr,
1913-
)
1907+
Some(EntityType::Specified(var_name)) => {
1908+
let all_rhs_known = rhs
1909+
.iter()
1910+
.all(|e| self.schema.euid_has_known_entity_type(e));
1911+
if self.schema.is_known_entity_type(var_name) && all_rhs_known {
1912+
let descendants = self.schema.get_entity_types_in_set(rhs.iter());
1913+
Typechecker::entity_in_descendants(
1914+
var_name,
1915+
descendants,
1916+
in_expr,
1917+
lhs_expr,
1918+
rhs_expr,
1919+
)
1920+
} else {
1921+
let annotated_expr =
1922+
ExprBuilder::with_data(Some(Type::primitive_boolean()))
1923+
.with_same_source_info(in_expr)
1924+
.is_in(lhs_expr, rhs_expr);
1925+
if self.mode.is_partial() {
1926+
// In partial schema mode, undeclared entity types are
1927+
// expected.
1928+
TypecheckAnswer::success(annotated_expr)
1929+
} else {
1930+
TypecheckAnswer::fail(annotated_expr)
1931+
}
1932+
}
19141933
}
19151934
// Unspecified entities will be detected by a different part of the validator.
19161935
// Still return `TypecheckFail` so that typechecking is not considered successful.
1917-
(Some(EntityType::Unspecified), _) => TypecheckAnswer::fail(
1936+
Some(EntityType::Unspecified) => TypecheckAnswer::fail(
19181937
ExprBuilder::with_data(Some(Type::primitive_boolean()))
19191938
.with_same_source_info(in_expr)
19201939
.is_in(lhs_expr, rhs_expr),
@@ -1966,7 +1985,7 @@ impl<'a> Typechecker<'a> {
19661985
} else if !lhs_is_action && !non_actions.is_empty() {
19671986
self.type_of_non_action_in_entities(
19681987
lhs_euid,
1969-
non_actions.iter(),
1988+
&non_actions,
19701989
in_expr,
19711990
lhs_expr,
19721991
rhs_expr,
@@ -2038,34 +2057,33 @@ impl<'a> Typechecker<'a> {
20382057
fn type_of_non_action_in_entities<'b>(
20392058
&self,
20402059
lhs: &EntityUID,
2041-
rhs: impl IntoIterator<Item = &'a EntityUID> + 'a,
2060+
rhs: &Vec<EntityUID>,
20422061
in_expr: &Expr,
20432062
lhs_expr: Expr<Option<Type>>,
20442063
rhs_expr: Expr<Option<Type>>,
20452064
) -> TypecheckAnswer<'b> {
20462065
match lhs.entity_type() {
20472066
EntityType::Specified(lhs_ety) => {
2048-
let rhs_descendants = self.schema.get_entity_types_in_set(rhs);
2049-
match rhs_descendants {
2050-
Some(rhs_descendants) if self.schema.is_known_entity_type(lhs_ety) => {
2051-
Typechecker::entity_in_descendants(
2052-
lhs_ety,
2053-
rhs_descendants,
2054-
in_expr,
2055-
lhs_expr,
2056-
rhs_expr,
2057-
)
2058-
}
2059-
_ => {
2060-
let annotated_expr =
2061-
ExprBuilder::with_data(Some(Type::primitive_boolean()))
2062-
.with_same_source_info(in_expr)
2063-
.is_in(lhs_expr, rhs_expr);
2064-
if self.mode.is_partial() {
2065-
TypecheckAnswer::success(annotated_expr)
2066-
} else {
2067-
TypecheckAnswer::fail(annotated_expr)
2068-
}
2067+
let all_rhs_known = rhs
2068+
.iter()
2069+
.all(|e| self.schema.euid_has_known_entity_type(e));
2070+
if self.schema.is_known_entity_type(lhs_ety) && all_rhs_known {
2071+
let rhs_descendants = self.schema.get_entity_types_in_set(rhs.iter());
2072+
Typechecker::entity_in_descendants(
2073+
lhs_ety,
2074+
rhs_descendants,
2075+
in_expr,
2076+
lhs_expr,
2077+
rhs_expr,
2078+
)
2079+
} else {
2080+
let annotated_expr = ExprBuilder::with_data(Some(Type::primitive_boolean()))
2081+
.with_same_source_info(in_expr)
2082+
.is_in(lhs_expr, rhs_expr);
2083+
if self.mode.is_partial() {
2084+
TypecheckAnswer::success(annotated_expr)
2085+
} else {
2086+
TypecheckAnswer::fail(annotated_expr)
20692087
}
20702088
}
20712089
}

cedar-policy-validator/src/typecheck/test_policy.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,66 @@ fn policy_in_action_impossible() {
427427
p.clone(),
428428
vec![TypeError::impossible_policy(p.condition())],
429429
);
430+
431+
let p = parse_policy(
432+
Some("0".to_string()),
433+
r#"permit(principal, action, resource) when { User::"alice" in [Action::"view_photo"] };"#,
434+
)
435+
.expect("Policy should parse.");
436+
assert_policy_typecheck_fails_simple_schema(
437+
p.clone(),
438+
vec![TypeError::impossible_policy(p.condition())],
439+
);
440+
441+
let p = parse_policy(
442+
Some("0".to_string()),
443+
r#"permit(principal, action, resource) when { principal in [action] };"#,
444+
)
445+
.expect("Policy should parse.");
446+
assert_policy_typecheck_fails_simple_schema(
447+
p.clone(),
448+
vec![TypeError::impossible_policy(p.condition())],
449+
);
450+
451+
let p = parse_policy(
452+
Some("0".to_string()),
453+
r#"permit(principal, action, resource) when { principal in action };"#,
454+
)
455+
.expect("Policy should parse.");
456+
assert_policy_typecheck_fails_simple_schema(
457+
p.clone(),
458+
vec![TypeError::impossible_policy(p.condition())],
459+
);
460+
461+
let p = parse_policy(
462+
Some("0".to_string()),
463+
r#"permit(principal, action, resource) when { principal in Action::"view_photo" };"#,
464+
)
465+
.expect("Policy should parse.");
466+
assert_policy_typecheck_fails_simple_schema(
467+
p.clone(),
468+
vec![TypeError::impossible_policy(p.condition())],
469+
);
470+
471+
let p = parse_policy(
472+
Some("0".to_string()),
473+
r#"permit(principal, action, resource) when { principal in [Action::"view_photo", Action::"delete_group"] };"#,
474+
)
475+
.expect("Policy should parse.");
476+
assert_policy_typecheck_fails_simple_schema(
477+
p.clone(),
478+
vec![TypeError::impossible_policy(p.condition())],
479+
);
480+
481+
let p = parse_policy(
482+
Some("0".to_string()),
483+
r#"permit(principal, action, resource) when { principal in [Action::"view_photo", Photo::"bar"] };"#,
484+
)
485+
.expect("Policy should parse.");
486+
assert_policy_typecheck_permissive_fails_simple_schema(
487+
p.clone(),
488+
vec![TypeError::impossible_policy(p.condition())],
489+
);
430490
}
431491

432492
#[test]

0 commit comments

Comments
 (0)