#![cfg(test)]
#![allow(clippy::panic)]
use super::*;
use authorizer::Decision;
use cedar_policy_core::ast;
use cedar_policy_core::authorizer;
use cedar_policy_core::entities::{self};
use cedar_policy_core::parser::err::ParseErrors;
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
mod entity_uid_tests {
use super::*;
use cool_asserts::assert_matches;
#[test]
fn entity_uid_from_parts() {
let entity_id = EntityId::from_str("bobby").expect("failed at constructing EntityId");
let entity_type_name = EntityTypeName::from_str("Chess::Master")
.expect("failed at constructing EntityTypeName");
let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
assert_eq!(euid.id().as_ref(), "bobby");
assert_eq!(euid.type_name().to_string(), "Chess::Master");
assert_eq!(euid.type_name().basename(), "Master");
assert_eq!(euid.type_name().namespace(), "Chess");
assert_eq!(euid.type_name().namespace_components().count(), 1);
}
#[test]
fn entity_uid_no_namespace() {
let entity_id = EntityId::from_str("bobby").expect("failed at constructing EntityId");
let entity_type_name =
EntityTypeName::from_str("User").expect("failed at constructing EntityTypeName");
let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
assert_eq!(euid.id().as_ref(), "bobby");
assert_eq!(euid.type_name().to_string(), "User");
assert_eq!(euid.type_name().basename(), "User");
assert_eq!(euid.type_name().namespace(), String::new());
assert_eq!(euid.type_name().namespace_components().count(), 0);
}
#[test]
fn entity_uid_nested_namespaces() {
let entity_id = EntityId::from_str("bobby").expect("failed at constructing EntityId");
let entity_type_name = EntityTypeName::from_str("A::B::C::D::Z")
.expect("failed at constructing EntityTypeName");
let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
assert_eq!(euid.id().as_ref(), "bobby");
assert_eq!(euid.type_name().to_string(), "A::B::C::D::Z");
assert_eq!(euid.type_name().basename(), "Z");
assert_eq!(euid.type_name().namespace(), "A::B::C::D");
assert_eq!(euid.type_name().namespace_components().count(), 4);
}
#[test]
fn entity_uid_with_escape() {
let entity_id = EntityId::from_str(r"bobby\'s sister:\nVeronica")
.expect("failed at constructing EntityId");
let entity_type_name = EntityTypeName::from_str("Hockey::Master")
.expect("failed at constructing EntityTypeName");
let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
assert_eq!(euid.id().as_ref(), r"bobby\'s sister:\nVeronica");
assert_eq!(euid.type_name().to_string(), "Hockey::Master");
assert_eq!(euid.type_name().basename(), "Master");
assert_eq!(euid.type_name().namespace(), "Hockey");
assert_eq!(euid.type_name().namespace_components().count(), 1);
}
#[test]
fn entity_uid_with_backslashes() {
let entity_id =
EntityId::from_str(r#"\ \a \b \' \" \\"#).expect("failed at constructing EntityId");
let entity_type_name =
EntityTypeName::from_str("Test::User").expect("failed at constructing EntityTypeName");
let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
assert_eq!(euid.id().as_ref(), r#"\ \a \b \' \" \\"#);
assert_eq!(euid.type_name().to_string(), "Test::User");
}
#[test]
fn entity_uid_with_quotes() {
let euid: EntityUid = EntityUid::from_type_name_and_id(
EntityTypeName::from_str("Test::User").unwrap(),
EntityId::from_str(r#"b'ob"by\'s sis\"ter"#).unwrap(),
);
assert_eq!(euid.id().as_ref(), r#"b'ob"by\'s sis\"ter"#);
assert_eq!(euid.type_name().to_string(), r"Test::User");
}
#[test]
fn entity_uid_with_whitespace() {
EntityTypeName::from_str("A :: B::C").expect_err("should fail due to RFC 9");
EntityTypeName::from_str(" A :: B\n::C \n ::D\n").expect_err("should fail due to RFC 9");
let policy = Policy::from_str(
r#"permit(principal == A :: B::C :: " hi there are spaces ", action, resource);"#,
)
.expect("should succeed, see RFC 9");
let PrincipalConstraint::Eq(euid) = policy.principal_constraint() else {
panic!("expected `Eq` constraint");
};
assert_eq!(euid.id().as_ref(), " hi there are spaces ");
assert_eq!(euid.type_name().to_string(), "A::B::C"); assert_eq!(euid.type_name().basename(), "C");
assert_eq!(euid.type_name().namespace(), "A::B");
assert_eq!(euid.type_name().namespace_components().count(), 2);
let policy = Policy::from_str(
r#"
permit(principal == A :: B
::C
:: D
:: " hi there are
spaces and
newlines ", action, resource);"#,
)
.expect("should succeed, see RFC 9");
let PrincipalConstraint::Eq(euid) = policy.principal_constraint() else {
panic!("expected `Eq` constraint")
};
assert_eq!(
euid.id().as_ref(),
" hi there are\n spaces and\n newlines "
);
assert_eq!(euid.type_name().to_string(), "A::B::C::D"); assert_eq!(euid.type_name().basename(), "D");
assert_eq!(euid.type_name().namespace(), "A::B::C");
assert_eq!(euid.type_name().namespace_components().count(), 3);
}
#[test]
fn malformed_entity_type_name_should_fail() {
let result = EntityTypeName::from_str("I'm an invalid name");
assert_matches!(result, Err(ParseErrors(_)));
let error = result.err().unwrap();
assert!(error.to_string().contains("invalid token"));
}
#[test]
fn parse_euid() {
let parsed_eid: EntityUid = r#"Test::User::"bobby""#.parse().expect("Failed to parse");
assert_eq!(parsed_eid.id().as_ref(), r"bobby");
assert_eq!(parsed_eid.type_name().to_string(), r"Test::User");
}
#[test]
fn parse_euid_with_escape() {
let parsed_eid: EntityUid = r#"Test::User::"b\'ob\"by""#.parse().expect("Failed to parse");
assert_eq!(parsed_eid.id().as_ref(), r#"b'ob"by"#);
assert_eq!(parsed_eid.type_name().to_string(), r"Test::User");
}
#[test]
fn parse_euid_single_quotes() {
let euid_str = r#"Test::User::"b'obby\'s sister""#;
EntityUid::from_str(euid_str).expect_err("Should fail, not normalized -- see RFC 9");
let policy_str = "permit(principal == ".to_string() + euid_str + ", action, resource);";
let policy = Policy::from_str(&policy_str).expect("Should parse; see RFC 9");
let PrincipalConstraint::Eq(parsed_euid) = policy.principal_constraint() else {
panic!("Expected an Eq constraint");
};
assert_eq!(parsed_euid.id().as_ref(), r"b'obby's sister");
assert_eq!(parsed_euid.type_name().to_string(), r"Test::User");
}
#[test]
fn parse_euid_whitespace() {
let euid_str = " A ::B :: C:: D \n :: \n E\n :: \"hi\"";
EntityUid::from_str(euid_str).expect_err("Should fail, not normalized -- see RFC 9");
let policy_str = "permit(principal == ".to_string() + euid_str + ", action, resource);";
let policy = Policy::from_str(&policy_str).expect("Should parse; see RFC 9");
let PrincipalConstraint::Eq(parsed_euid) = policy.principal_constraint() else {
panic!("Expected an Eq constraint");
};
assert_eq!(parsed_euid.id().as_ref(), "hi");
assert_eq!(parsed_euid.type_name().to_string(), "A::B::C::D::E"); assert_eq!(parsed_euid.type_name().basename(), "E");
assert_eq!(parsed_euid.type_name().namespace(), "A::B::C::D");
assert_eq!(parsed_euid.type_name().namespace_components().count(), 4);
}
#[test]
fn euid_roundtrip() {
let parsed_euid: EntityUid = r#"Test::User::"b\'ob""#.parse().expect("Failed to parse");
assert_eq!(parsed_euid.id().as_ref(), r"b'ob");
let reparsed: EntityUid = format!("{parsed_euid}")
.parse()
.expect("failed to roundtrip");
assert_eq!(reparsed.id().as_ref(), r"b'ob");
}
#[test]
fn accessing_unspecified_entity_returns_none() {
let c = Context::empty();
let request = Request::new(None, None, None, c, None).unwrap();
let p = request.principal();
let a = request.action();
let r = request.resource();
assert_matches!(p, None);
assert_matches!(a, None);
assert_matches!(r, None);
}
}
mod scope_constraints_tests {
use super::*;
#[test]
fn principal_constraint_inline() {
let p = Policy::from_str("permit(principal,action,resource);").unwrap();
assert_eq!(p.principal_constraint(), PrincipalConstraint::Any);
let euid = EntityUid::from_strs("T", "a");
assert_eq!(euid.id().as_ref(), "a");
assert_eq!(
euid.type_name(),
&EntityTypeName::from_str("T").expect("Failed to parse EntityTypeName")
);
let p =
Policy::from_str("permit(principal == T::\"a\",action,resource == T::\"b\");").unwrap();
assert_eq!(
p.principal_constraint(),
PrincipalConstraint::Eq(euid.clone())
);
let p = Policy::from_str("permit(principal in T::\"a\",action,resource);").unwrap();
assert_eq!(
p.principal_constraint(),
PrincipalConstraint::In(euid.clone())
);
let p = Policy::from_str("permit(principal is T,action,resource);").unwrap();
assert_eq!(
p.principal_constraint(),
PrincipalConstraint::Is(EntityTypeName::from_str("T").unwrap())
);
let p = Policy::from_str("permit(principal is T in T::\"a\",action,resource);").unwrap();
assert_eq!(
p.principal_constraint(),
PrincipalConstraint::IsIn(EntityTypeName::from_str("T").unwrap(), euid)
);
}
#[test]
fn action_constraint_inline() {
let p = Policy::from_str("permit(principal,action,resource);").unwrap();
assert_eq!(p.action_constraint(), ActionConstraint::Any);
let euid = EntityUid::from_strs("NN::N::Action", "a");
assert_eq!(
euid.type_name(),
&EntityTypeName::from_str("NN::N::Action").expect("Failed to parse EntityTypeName")
);
let p = Policy::from_str(
"permit(principal == T::\"b\",action == NN::N::Action::\"a\",resource == T::\"c\");",
)
.unwrap();
assert_eq!(p.action_constraint(), ActionConstraint::Eq(euid.clone()));
let p = Policy::from_str("permit(principal,action in [NN::N::Action::\"a\"],resource);")
.unwrap();
assert_eq!(p.action_constraint(), ActionConstraint::In(vec![euid]));
}
#[test]
fn resource_constraint_inline() {
let p = Policy::from_str("permit(principal,action,resource);").unwrap();
assert_eq!(p.resource_constraint(), ResourceConstraint::Any);
let euid = EntityUid::from_strs("NN::N::T", "a");
assert_eq!(
euid.type_name(),
&EntityTypeName::from_str("NN::N::T").expect("Failed to parse EntityTypeName")
);
let p =
Policy::from_str("permit(principal == T::\"b\",action,resource == NN::N::T::\"a\");")
.unwrap();
assert_eq!(
p.resource_constraint(),
ResourceConstraint::Eq(euid.clone())
);
let p = Policy::from_str("permit(principal,action,resource in NN::N::T::\"a\");").unwrap();
assert_eq!(
p.resource_constraint(),
ResourceConstraint::In(euid.clone())
);
let p = Policy::from_str("permit(principal,action,resource is NN::N::T);").unwrap();
assert_eq!(
p.resource_constraint(),
ResourceConstraint::Is(EntityTypeName::from_str("NN::N::T").unwrap())
);
let p =
Policy::from_str("permit(principal,action,resource is NN::N::T in NN::N::T::\"a\");")
.unwrap();
assert_eq!(
p.resource_constraint(),
ResourceConstraint::IsIn(EntityTypeName::from_str("NN::N::T").unwrap(), euid)
);
}
#[test]
fn principal_constraint_link() {
let euid = EntityUid::from_strs("T", "a");
let map: HashMap<SlotId, EntityUid> =
std::iter::once((SlotId::principal(), euid.clone())).collect();
let p = link(
"permit(principal in ?principal,action,resource);",
map.clone(),
);
assert_eq!(
p.principal_constraint(),
PrincipalConstraint::In(euid.clone())
);
let p = link(
"permit(principal == ?principal,action,resource);",
map.clone(),
);
assert_eq!(
p.principal_constraint(),
PrincipalConstraint::Eq(euid.clone())
);
}
#[test]
fn resource_constraint_link() {
let euid = EntityUid::from_strs("T", "a");
let map: HashMap<SlotId, EntityUid> =
std::iter::once((SlotId::resource(), euid.clone())).collect();
let p = link(
"permit(principal,action,resource in ?resource);",
map.clone(),
);
assert_eq!(
p.resource_constraint(),
ResourceConstraint::In(euid.clone())
);
let p = link(
"permit(principal,action,resource == ?resource);",
map.clone(),
);
assert_eq!(
p.resource_constraint(),
ResourceConstraint::Eq(euid.clone())
);
let p = link("permit(principal,action,resource is T in ?resource);", map);
assert_eq!(
p.resource_constraint(),
ResourceConstraint::IsIn(EntityTypeName::from_str("T").unwrap(), euid)
);
}
#[track_caller]
fn link(src: &str, values: HashMap<SlotId, EntityUid>) -> Policy {
let mut pset = PolicySet::new();
let template = Template::parse(Some("Id".to_string()), src).unwrap();
pset.add_template(template).unwrap();
let link_id = PolicyId::from_str("link").unwrap();
pset.link(PolicyId::from_str("Id").unwrap(), link_id.clone(), values)
.unwrap();
pset.policy(&link_id).unwrap().clone()
}
}
mod policy_set_tests {
use super::*;
use ast::LinkingError;
use cool_asserts::assert_matches;
#[test]
fn no_unknown_feature() {
let src = r#"
permit(principal,action,resource) when {
unknown("foo")
};
"#;
let pset: Result<PolicySet, _> = src.parse();
#[cfg(not(feature = "partial-eval"))]
{
let err_string = pset.unwrap_err().to_string();
assert!(err_string.contains("`unknown` is not a function"));
}
#[cfg(feature = "partial-eval")]
{
assert!(pset.is_ok());
}
}
#[test]
fn template_link_lookup() {
let mut pset = PolicySet::new();
let p = Policy::parse(Some("p".into()), "permit(principal,action,resource);")
.expect("Failed to parse");
pset.add(p).expect("Failed to add");
let template = Template::parse(
Some("t".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Failed to parse");
pset.add_template(template).expect("Add failed");
let env: HashMap<SlotId, EntityUid> =
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
pset.link(
PolicyId::from_str("t").unwrap(),
PolicyId::from_str("id").unwrap(),
env.clone(),
)
.expect("Failed to link");
let p0 = pset.policy(&PolicyId::from_str("p").unwrap()).unwrap();
let tp = pset.policy(&PolicyId::from_str("id").unwrap()).unwrap();
assert_eq!(
p0.template_links(),
None,
"A normal policy should not have template links"
);
assert_eq!(
tp.template_links(),
Some(env),
"A template-linked policy's links should be stored properly"
);
}
#[test]
fn link_conflicts() {
let mut pset = PolicySet::new();
let p1 = Policy::parse(Some("id".into()), "permit(principal,action,resource);")
.expect("Failed to parse");
pset.add(p1).expect("Failed to add");
let template = Template::parse(
Some("t".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Failed to parse");
pset.add_template(template).expect("Add failed");
let env: HashMap<SlotId, EntityUid> =
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
let before_link = pset.clone();
let r = pset.link(
PolicyId::from_str("t").unwrap(),
PolicyId::from_str("id").unwrap(),
env,
);
assert_matches!(
r,
Err(PolicySetError::LinkingError(LinkingError::PolicyIdConflict { id })) =>{
assert_eq!(id, ast::PolicyID::from_string("id"));
}
);
assert_eq!(
pset, before_link,
"A failed link shouldn't mutate the policy set"
);
}
#[test]
fn policyset_add() {
let mut pset = PolicySet::new();
let static_policy = Policy::parse(Some("id".into()), "permit(principal,action,resource);")
.expect("Failed to parse");
pset.add(static_policy).expect("Failed to add");
let template = Template::parse(
Some("t".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Failed to parse");
pset.add_template(template).expect("Failed to add");
let env1: HashMap<SlotId, EntityUid> =
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test1"))).collect();
pset.link(
PolicyId::from_str("t").unwrap(),
PolicyId::from_str("link").unwrap(),
env1,
)
.expect("Failed to link");
let env2: HashMap<SlotId, EntityUid> =
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test2"))).collect();
let err = pset
.link(
PolicyId::from_str("t").unwrap(),
PolicyId::from_str("link").unwrap(),
env2.clone(),
)
.expect_err("Should have failed due to conflict with existing link id");
match err {
PolicySetError::LinkingError(_) => (),
e => panic!("Wrong error: {e}"),
}
pset.link(
PolicyId::from_str("t").unwrap(),
PolicyId::from_str("link2").unwrap(),
env2,
)
.expect("Failed to link");
let template2 = Template::parse(
Some("t".into()),
"forbid(principal, action, resource == ?resource);",
)
.expect("Failed to parse");
pset.add_template(template2)
.expect_err("should have failed due to conflict on template id");
let template2 = Template::parse(
Some("t2".into()),
"forbid(principal, action, resource == ?resource);",
)
.expect("Failed to parse");
pset.add_template(template2)
.expect("Failed to add template");
let env3: HashMap<SlotId, EntityUid> =
std::iter::once((SlotId::resource(), EntityUid::from_strs("Test", "test3"))).collect();
pset.link(
PolicyId::from_str("t").unwrap(),
PolicyId::from_str("unique3").unwrap(),
env3.clone(),
)
.expect_err("should have failed due to conflict on template id");
pset.link(
PolicyId::from_str("t2").unwrap(),
PolicyId::from_str("unique3").unwrap(),
env3,
)
.expect("should succeed with unique ids");
}
#[test]
fn policyset_remove() {
let authorizer = Authorizer::new();
let request = Request::new(
Some(EntityUid::from_strs("Test", "test")),
Some(EntityUid::from_strs("Action", "a")),
Some(EntityUid::from_strs("Resource", "b")),
Context::empty(),
None,
)
.unwrap();
let e = r#"[
{
"uid": {"type":"Test","id":"test"},
"attrs": {},
"parents": []
},
{
"uid": {"type":"Action","id":"a"},
"attrs": {},
"parents": []
},
{
"uid": {"type":"Resource","id":"b"},
"attrs": {},
"parents": []
}
]"#;
let entities = Entities::from_json_str(e, None).expect("entity error");
let mut pset = PolicySet::new();
let static_policy = Policy::parse(Some("id".into()), "permit(principal,action,resource);")
.expect("Failed to parse");
pset.add(static_policy).expect("Failed to add");
let response = authorizer.is_authorized(&request, &pset, &entities);
assert_eq!(response.decision(), Decision::Allow);
pset.remove_static(PolicyId::from_str("id").unwrap())
.expect("Failed to remove static policy");
let response = authorizer.is_authorized(&request, &pset, &entities);
assert_eq!(response.decision(), Decision::Deny);
let template = Template::parse(
Some("t".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Failed to parse");
pset.add_template(template).expect("Failed to add");
let linked_policy_id = PolicyId::from_str("linked").unwrap();
let env1: HashMap<SlotId, EntityUid> =
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
pset.link(
PolicyId::from_str("t").unwrap(),
linked_policy_id.clone(),
env1,
)
.expect("Failed to link");
let response = authorizer.is_authorized(&request, &pset, &entities);
assert_eq!(response.decision(), Decision::Allow);
assert_matches!(
pset.remove_static(PolicyId::from_str("t").unwrap()),
Err(PolicySetError::PolicyNonexistentError(_))
);
let result = pset.unlink(linked_policy_id.clone());
assert_matches!(result, Ok(_));
assert_matches!(
pset.remove_static(PolicyId::from_str("t").unwrap()),
Err(PolicySetError::PolicyNonexistentError(_))
);
let response = authorizer.is_authorized(&request, &pset, &entities);
assert_eq!(response.decision(), Decision::Deny);
let env1: HashMap<SlotId, EntityUid> =
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
pset.link(
PolicyId::from_str("t").unwrap(),
linked_policy_id.clone(),
env1,
)
.expect("Failed to link");
let response = authorizer.is_authorized(&request, &pset, &entities);
assert_eq!(response.decision(), Decision::Allow);
assert_matches!(
pset.remove_template(PolicyId::from_str("t").unwrap()),
Err(PolicySetError::RemoveTemplateWithActiveLinksError(_))
);
let result = pset.unlink(linked_policy_id);
assert_matches!(result, Ok(_));
pset.remove_template(PolicyId::from_str("t").unwrap())
.expect("Failed to remove policy template");
let response = authorizer.is_authorized(&request, &pset, &entities);
assert_eq!(response.decision(), Decision::Deny);
}
#[test]
fn pset_removal_prop_test_1() {
let template = Template::parse(
Some("policy0".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
let mut pset = PolicySet::new();
pset.add_template(template).unwrap();
let env: HashMap<SlotId, EntityUid> =
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
pset.link(
PolicyId::from_str("policy0").unwrap(),
PolicyId::from_str("policy3").unwrap(),
env,
)
.unwrap();
let template = Template::parse(
Some("policy3".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
assert_matches!(
pset.add_template(template),
Err(PolicySetError::AlreadyDefined { .. })
);
assert_matches!(
pset.remove_static(PolicyId::from_str("policy3").unwrap()),
Err(PolicySetError::PolicyNonexistentError(_))
);
assert_matches!(
pset.remove_template(PolicyId::from_str("policy3").unwrap()),
Err(PolicySetError::TemplateNonexistentError(_))
);
}
#[test]
fn pset_requests() {
let template = Template::parse(
Some("template".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
let static_policy = Policy::parse(
Some("static".into()),
"permit(principal, action, resource);",
)
.expect("Static parse failure");
let mut pset = PolicySet::new();
pset.add_template(template).unwrap();
pset.add(static_policy).unwrap();
pset.link(
PolicyId::from_str("template").unwrap(),
PolicyId::from_str("linked").unwrap(),
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
)
.expect("Link failure");
assert_eq!(pset.num_of_templates(), 1);
assert_eq!(pset.num_of_policies(), 2);
assert_eq!(pset.policies().filter(|p| p.is_static()).count(), 1);
assert_eq!(
pset.template(&"template".parse().unwrap())
.expect("lookup failed")
.id(),
&"template".parse().unwrap()
);
assert_eq!(
pset.policy(&"static".parse().unwrap())
.expect("lookup failed")
.id(),
&"static".parse().unwrap()
);
assert_eq!(
pset.policy(&"linked".parse().unwrap())
.expect("lookup failed")
.id(),
&"linked".parse().unwrap()
);
}
#[test]
fn link_static_policy() {
let static_policy = Policy::parse(
Some("static".into()),
"permit(principal, action, resource);",
)
.expect("Static parse failure");
let mut pset = PolicySet::new();
pset.add(static_policy).unwrap();
let before_link = pset.clone();
let result = pset.link(
PolicyId::from_str("static").unwrap(),
PolicyId::from_str("linked").unwrap(),
HashMap::new(),
);
assert_matches!(result, Err(PolicySetError::ExpectedTemplate));
assert_eq!(
pset, before_link,
"A failed link shouldn't mutate the policy set"
);
}
#[test]
fn link_linked_policy() {
let template = Template::parse(
Some("template".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
let mut pset = PolicySet::new();
pset.add_template(template).unwrap();
pset.link(
PolicyId::from_str("template").unwrap(),
PolicyId::from_str("linked").unwrap(),
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
)
.unwrap();
let before_link = pset.clone();
let result = pset.link(
PolicyId::from_str("linked").unwrap(),
PolicyId::from_str("linked2").unwrap(),
HashMap::new(),
);
assert_matches!(result, Err(PolicySetError::ExpectedTemplate));
assert_eq!(
pset, before_link,
"A failed link shouldn't mutate the policy set"
);
}
#[cfg(feature = "partial-eval")]
#[test]
fn unknown_entities() {
let ast = ast::Policy::from_when_clause(
ast::Effect::Permit,
ast::Expr::unknown(ast::Unknown::new_with_type(
"test_entity_type::\"unknown\"",
ast::Type::Entity {
ty: ast::EntityType::Specified("test_entity_type".parse().unwrap()),
},
)),
ast::PolicyID::from_smolstr("static".into()),
None,
);
let static_policy = Policy::from_ast(ast);
let mut pset = PolicySet::new();
pset.add(static_policy).unwrap();
let entity_uids = pset.unknown_entities();
entity_uids.contains(&"test_entity_type::\"unknown\"".parse().unwrap());
}
#[test]
fn unlink_linked_policy() {
let template = Template::parse(
Some("template".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
let mut pset = PolicySet::new();
pset.add_template(template).unwrap();
let linked_policy_id = PolicyId::from_str("linked").unwrap();
pset.link(
PolicyId::from_str("template").unwrap(),
linked_policy_id.clone(),
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
)
.unwrap();
let authorizer = Authorizer::new();
let request = Request::new(
Some(EntityUid::from_strs("Test", "test")),
Some(EntityUid::from_strs("Action", "a")),
Some(EntityUid::from_strs("Resource", "b")),
Context::empty(),
None,
)
.unwrap();
let e = r#"[
{
"uid": {"type":"Test","id":"test"},
"attrs": {},
"parents": []
},
{
"uid": {"type":"Action","id":"a"},
"attrs": {},
"parents": []
},
{
"uid": {"type":"Resource","id":"b"},
"attrs": {},
"parents": []
}
]"#;
let entities = Entities::from_json_str(e, None).expect("entity error");
let response = authorizer.is_authorized(&request, &pset, &entities);
assert_eq!(response.decision(), Decision::Allow);
let result = pset.unlink(linked_policy_id.clone());
assert_matches!(result, Ok(_));
let response = authorizer.is_authorized(&request, &pset, &entities);
assert_eq!(response.decision(), Decision::Deny);
let result = pset.unlink(linked_policy_id);
assert_matches!(result, Err(PolicySetError::LinkNonexistentError(_)));
}
#[test]
fn get_linked_policy() {
let mut pset = PolicySet::new();
let template = Template::parse(
Some("template".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
pset.add_template(template).unwrap();
let linked_policy_id = PolicyId::from_str("linked").unwrap();
pset.link(
PolicyId::from_str("template").unwrap(),
linked_policy_id.clone(),
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
)
.unwrap();
assert_eq!(
pset.get_linked_policies(PolicyId::from_str("template").unwrap())
.unwrap()
.count(),
1
);
let result = pset.unlink(linked_policy_id.clone());
assert_matches!(result, Ok(_));
assert_eq!(
pset.get_linked_policies(PolicyId::from_str("template").unwrap())
.unwrap()
.count(),
0
);
let result = pset.unlink(linked_policy_id.clone());
assert_matches!(result, Err(PolicySetError::LinkNonexistentError(_)));
pset.link(
PolicyId::from_str("template").unwrap(),
linked_policy_id.clone(),
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
)
.unwrap();
assert_eq!(
pset.get_linked_policies(PolicyId::from_str("template").unwrap())
.unwrap()
.count(),
1
);
pset.link(
PolicyId::from_str("template").unwrap(),
PolicyId::from_str("linked2").unwrap(),
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
)
.unwrap();
assert_eq!(
pset.get_linked_policies(PolicyId::from_str("template").unwrap())
.unwrap()
.count(),
2
);
let template = Template::parse(
Some("template".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
assert_matches!(
pset.add_template(template),
Err(PolicySetError::AlreadyDefined { .. })
);
let template = Template::parse(
Some("template2".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
pset.add_template(template).unwrap();
assert_eq!(
pset.get_linked_policies(PolicyId::from_str("template2").unwrap())
.unwrap()
.count(),
0
);
assert_eq!(
pset.get_linked_policies(PolicyId::from_str("template").unwrap())
.unwrap()
.count(),
2
);
assert_matches!(
pset.remove_template(PolicyId::from_str("template").unwrap()),
Err(PolicySetError::RemoveTemplateWithActiveLinksError(_))
);
let illegal_template_policy = Policy::parse(
Some("template".into()),
"permit(principal, action, resource);",
)
.expect("Static parse failure");
assert_matches!(
pset.add(illegal_template_policy),
Err(PolicySetError::AlreadyDefined { .. })
);
let illegal_linked_policy = Policy::parse(
Some("linked".into()),
"permit(principal, action, resource);",
)
.expect("Static parse failure");
assert_matches!(
pset.add(illegal_linked_policy),
Err(PolicySetError::AlreadyDefined { .. })
);
let static_policy = Policy::parse(
Some("policy".into()),
"permit(principal, action, resource);",
)
.expect("Static parse failure");
pset.add(static_policy).unwrap();
pset.remove_static(PolicyId::from_str("policy").unwrap())
.expect("should be able to remove policy");
assert_matches!(
pset.remove_static(PolicyId::from_str("linked").unwrap()),
Err(PolicySetError::PolicyNonexistentError(_))
);
assert_matches!(
pset.remove_static(PolicyId::from_str("template").unwrap()),
Err(PolicySetError::PolicyNonexistentError(_))
);
assert_eq!(
pset.get_linked_policies(PolicyId::from_str("template").unwrap())
.unwrap()
.count(),
2
);
let result = pset.unlink(linked_policy_id);
assert_matches!(result, Ok(_));
assert_eq!(
pset.get_linked_policies(PolicyId::from_str("template").unwrap())
.unwrap()
.count(),
1
);
assert_matches!(
pset.remove_template(PolicyId::from_str("template2").unwrap()),
Ok(_)
);
assert_matches!(
pset.remove_template(PolicyId::from_str("template").unwrap()),
Err(PolicySetError::RemoveTemplateWithActiveLinksError(_))
);
let result = pset.unlink(PolicyId::from_str("linked2").unwrap());
assert_matches!(result, Ok(_));
assert_eq!(
pset.get_linked_policies(PolicyId::from_str("template").unwrap())
.unwrap()
.count(),
0
);
assert_matches!(
pset.remove_template(PolicyId::from_str("template").unwrap()),
Ok(_)
);
assert_matches!(
pset.get_linked_policies(PolicyId::from_str("template").unwrap())
.err()
.unwrap(),
PolicySetError::TemplateNonexistentError(_)
);
}
#[test]
fn pset_add_conflict() {
let template = Template::parse(
Some("policy0".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
let mut pset = PolicySet::new();
pset.add_template(template).unwrap();
let env: HashMap<SlotId, EntityUid> =
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
pset.link(
PolicyId::from_str("policy0").unwrap(),
PolicyId::from_str("policy1").unwrap(),
env,
)
.unwrap();
let static_policy = Policy::parse(
Some("policy0".into()),
"permit(principal, action, resource);",
)
.expect("Static parse failure");
assert_matches!(
pset.add(static_policy),
Err(PolicySetError::AlreadyDefined { .. })
);
let static_policy = Policy::parse(
Some("policy1".into()),
"permit(principal, action, resource);",
)
.expect("Static parse failure");
assert_matches!(
pset.add(static_policy),
Err(PolicySetError::AlreadyDefined { .. })
);
let static_policy = Policy::parse(
Some("policy2".into()),
"permit(principal, action, resource);",
)
.expect("Static parse failure");
pset.add(static_policy.clone()).unwrap();
assert_matches!(
pset.add(static_policy),
Err(PolicySetError::AlreadyDefined { .. })
);
}
#[test]
fn pset_add_template_conflict() {
let template = Template::parse(
Some("policy0".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
let mut pset = PolicySet::new();
pset.add_template(template).unwrap();
let env: HashMap<SlotId, EntityUid> =
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
pset.link(
PolicyId::from_str("policy0").unwrap(),
PolicyId::from_str("policy3").unwrap(),
env,
)
.unwrap();
let template = Template::parse(
Some("policy3".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
assert_matches!(
pset.add_template(template),
Err(PolicySetError::AlreadyDefined { .. })
);
let template = Template::parse(
Some("policy0".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
assert_matches!(
pset.add_template(template),
Err(PolicySetError::AlreadyDefined { .. })
);
let static_policy = Policy::parse(
Some("policy1".into()),
"permit(principal, action, resource);",
)
.expect("Static parse failure");
pset.add(static_policy).unwrap();
let template = Template::parse(
Some("policy1".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
assert_matches!(
pset.add_template(template),
Err(PolicySetError::AlreadyDefined { .. })
);
}
#[test]
fn pset_link_conflict() {
let template = Template::parse(
Some("policy0".into()),
"permit(principal == ?principal, action, resource);",
)
.expect("Template Parse Failure");
let mut pset = PolicySet::new();
pset.add_template(template).unwrap();
let env: HashMap<SlotId, EntityUid> =
std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
pset.link(
PolicyId::from_str("policy0").unwrap(),
PolicyId::from_str("policy3").unwrap(),
env.clone(),
)
.unwrap();
assert_matches!(
pset.link(
PolicyId::from_str("policy0").unwrap(),
PolicyId::from_str("policy3").unwrap(),
env.clone(),
),
Err(PolicySetError::LinkingError(
LinkingError::PolicyIdConflict { .. }
))
);
assert_matches!(
pset.link(
PolicyId::from_str("policy0").unwrap(),
PolicyId::from_str("policy0").unwrap(),
env.clone(),
),
Err(PolicySetError::LinkingError(
LinkingError::PolicyIdConflict { .. }
))
);
let static_policy = Policy::parse(
Some("policy1".into()),
"permit(principal, action, resource);",
)
.expect("Static parse failure");
pset.add(static_policy).unwrap();
assert_matches!(
pset.link(
PolicyId::from_str("policy0").unwrap(),
PolicyId::from_str("policy1").unwrap(),
env,
),
Err(PolicySetError::LinkingError(
LinkingError::PolicyIdConflict { .. }
))
);
}
}
mod schema_tests {
use super::*;
use cool_asserts::assert_matches;
use serde_json::json;
#[test]
fn valid_schema() {
Schema::from_json_value(json!(
{ "": {
"entityTypes": {
"Photo": {
"memberOfTypes": [ "Album" ],
"shape": {
"type": "Record",
"attributes": {
"foo": {
"type": "Boolean",
"required": false
}
}
}
},
"Album": {
"memberOfTypes": [ ],
"shape": {
"type": "Record",
"attributes": {
"foo": {
"type": "Boolean",
"required": false
}
}
}
}
},
"actions": {
"view": {
"appliesTo": {
"principalTypes": ["Photo", "Album"],
"resourceTypes": ["Photo"]
}
}
}
}}))
.expect("schema should be valid");
}
#[test]
fn invalid_schema() {
assert_matches!(
Schema::from_json_value(json!(
r#""{"": {
"entityTypes": {
"Photo": {
"memberOfTypes": [ "Album" ],
"shape": {
"type": "Record",
"attributes": {
"foo": {
"type": "Boolean",
"required": false
}
}
}
},
"Album": {
"memberOfTypes": [ ],
"shape": {
"type": "Record",
"attributes": {
"foo": {
"type": "Boolean",
"required": false
}
}
}
},
"Photo": {
"memberOfTypes": [ "Album" ],
"shape": {
"type": "Record",
"attributes": {
"foo": {
"type": "Boolean",
"required": false
}
}
}
}
},
"actions": {
"view": {
"appliesTo": {
"principalTypes": ["Photo", "Album"],
"resourceTypes": ["Photo"]
}
}
}
}}"#
)),
Err(SchemaError::Serde(_))
);
}
}
mod ancestors_tests {
use super::*;
#[test]
fn test_ancestors() {
let a_euid: EntityUid = EntityUid::from_strs("test", "A");
let b_euid: EntityUid = EntityUid::from_strs("test", "b");
let c_euid: EntityUid = EntityUid::from_strs("test", "C");
let a = Entity::new_no_attrs(a_euid.clone(), HashSet::new());
let b = Entity::new_no_attrs(b_euid.clone(), std::iter::once(a_euid.clone()).collect());
let c = Entity::new_no_attrs(c_euid.clone(), std::iter::once(b_euid.clone()).collect());
let es = Entities::from_entities([a, b, c], None).unwrap();
let ans = es.ancestors(&c_euid).unwrap().collect::<HashSet<_>>();
assert_eq!(ans.len(), 2);
assert!(ans.contains(&b_euid));
assert!(ans.contains(&a_euid));
}
}
mod entity_validate_tests {
use super::*;
use cool_asserts::assert_matches;
use entities::EntitiesError;
use serde_json::json;
fn schema() -> Schema {
Schema::from_json_value(json!(
{"": {
"entityTypes": {
"Employee": {
"memberOfTypes": [],
"shape": {
"type": "Record",
"attributes": {
"isFullTime": { "type": "Boolean" },
"numDirectReports": { "type": "Long" },
"department": { "type": "String" },
"manager": { "type": "Entity", "name": "Employee" },
"hr_contacts": { "type": "Set", "element": {
"type": "Entity", "name": "HR" } },
"json_blob": { "type": "Record", "attributes": {
"inner1": { "type": "Boolean" },
"inner2": { "type": "String" },
"inner3": { "type": "Record", "attributes": {
"innerinner": { "type": "Entity", "name": "Employee" }
}}
}},
"home_ip": { "type": "Extension", "name": "ipaddr" },
"work_ip": { "type": "Extension", "name": "ipaddr" },
"trust_score": { "type": "Extension", "name": "decimal" },
"tricky": { "type": "Record", "attributes": {
"type": { "type": "String" },
"id": { "type": "String" }
}}
}
}
},
"HR": {
"memberOfTypes": []
}
},
"actions": {
"view": { }
}
}}
))
.expect("should be a valid schema")
}
fn validate_entity(entity: Entity, schema: &Schema) -> Result<(), entities::EntitiesError> {
let _ = Entities::from_entities([entity], Some(schema))?;
Ok(())
}
#[test]
fn valid_entity() {
let entity = Entity::new(
EntityUid::from_strs("Employee", "123"),
HashMap::from_iter([
("isFullTime".into(), RestrictedExpression::new_bool(false)),
("numDirectReports".into(), RestrictedExpression::new_long(3)),
(
"department".into(),
RestrictedExpression::new_string("Sales".into()),
),
(
"manager".into(),
RestrictedExpression::from_str(r#"Employee::"456""#).unwrap(),
),
("hr_contacts".into(), RestrictedExpression::new_set([])),
(
"json_blob".into(),
RestrictedExpression::new_record([
("inner1".into(), RestrictedExpression::new_bool(false)),
(
"inner2".into(),
RestrictedExpression::new_string("foo".into()),
),
(
"inner3".into(),
RestrictedExpression::new_record([(
"innerinner".into(),
RestrictedExpression::from_str(r#"Employee::"abc""#).unwrap(),
)])
.unwrap(),
),
])
.unwrap(),
),
(
"home_ip".into(),
RestrictedExpression::from_str(r#"ip("10.20.30.40")"#).unwrap(),
),
(
"work_ip".into(),
RestrictedExpression::from_str(r#"ip("10.50.60.70")"#).unwrap(),
),
(
"trust_score".into(),
RestrictedExpression::from_str(r#"decimal("36.53")"#).unwrap(),
),
(
"tricky".into(),
RestrictedExpression::from_str(r#"{ type: "foo", id: "bar" }"#).unwrap(),
),
]),
HashSet::new(),
)
.unwrap();
validate_entity(entity.clone(), &schema()).unwrap();
let (uid, attrs, parents) = entity.into_inner();
validate_entity(Entity::new(uid, attrs, parents).unwrap(), &schema()).unwrap();
}
#[test]
fn invalid_entities() {
let schema = schema();
let entity = Entity::new(
EntityUid::from_strs("Employee", "123"),
HashMap::from_iter([
("isFullTime".into(), RestrictedExpression::new_bool(false)),
("numDirectReports".into(), RestrictedExpression::new_long(3)),
(
"department".into(),
RestrictedExpression::new_string("Sales".into()),
),
(
"manager".into(),
RestrictedExpression::from_str(r#"Employee::"456""#).unwrap(),
),
("hr_contacts".into(), RestrictedExpression::new_set([])),
(
"json_blob".into(),
RestrictedExpression::new_record([
("inner1".into(), RestrictedExpression::new_bool(false)),
(
"inner2".into(),
RestrictedExpression::new_string("foo".into()),
),
(
"inner3".into(),
RestrictedExpression::new_record([(
"innerinner".into(),
RestrictedExpression::from_str(r#"Employee::"abc""#).unwrap(),
)])
.unwrap(),
),
])
.unwrap(),
),
(
"home_ip".into(),
RestrictedExpression::from_str(r#"ip("10.20.30.40")"#).unwrap(),
),
(
"work_ip".into(),
RestrictedExpression::from_str(r#"ip("10.50.60.70")"#).unwrap(),
),
(
"trust_score".into(),
RestrictedExpression::from_str(r#"decimal("36.53")"#).unwrap(),
),
(
"tricky".into(),
RestrictedExpression::from_str(r#"{ type: "foo", id: "bar" }"#).unwrap(),
),
]),
HashSet::from_iter([EntityUid::from_strs("Manager", "jane")]),
)
.unwrap();
match validate_entity(entity, &schema) {
Ok(()) => panic!("expected an error due to extraneous parent"),
Err(e) => {
assert!(
e.to_string().contains(r#"`Employee::"123"` is not allowed to have an ancestor of type `Manager` according to the schema"#),
"actual error message was {e}",
);
}
}
let entity = Entity::new(
EntityUid::from_strs("Employee", "123"),
HashMap::from_iter([
("isFullTime".into(), RestrictedExpression::new_bool(false)),
(
"department".into(),
RestrictedExpression::new_string("Sales".into()),
),
(
"manager".into(),
RestrictedExpression::from_str(r#"Employee::"456""#).unwrap(),
),
("hr_contacts".into(), RestrictedExpression::new_set([])),
(
"json_blob".into(),
RestrictedExpression::new_record([
("inner1".into(), RestrictedExpression::new_bool(false)),
(
"inner2".into(),
RestrictedExpression::new_string("foo".into()),
),
(
"inner3".into(),
RestrictedExpression::new_record([(
"innerinner".into(),
RestrictedExpression::from_str(r#"Employee::"abc""#).unwrap(),
)])
.unwrap(),
),
])
.unwrap(),
),
(
"home_ip".into(),
RestrictedExpression::from_str(r#"ip("10.20.30.40")"#).unwrap(),
),
(
"work_ip".into(),
RestrictedExpression::from_str(r#"ip("10.50.60.70")"#).unwrap(),
),
(
"trust_score".into(),
RestrictedExpression::from_str(r#"decimal("36.53")"#).unwrap(),
),
(
"tricky".into(),
RestrictedExpression::from_str(r#"{ type: "foo", id: "bar" }"#).unwrap(),
),
]),
HashSet::new(),
)
.unwrap();
match validate_entity(entity, &schema) {
Ok(()) => panic!("expected an error due to missing attribute `numDirectReports`"),
Err(e) => {
assert!(
e.to_string().contains(r#"expected entity `Employee::"123"` to have attribute `numDirectReports`, but it does not"#),
"actual error message was {e}",
);
}
}
let entity = Entity::new(
EntityUid::from_strs("Employee", "123"),
HashMap::from_iter([
("isFullTime".into(), RestrictedExpression::new_bool(false)),
("extra".into(), RestrictedExpression::new_bool(true)),
("numDirectReports".into(), RestrictedExpression::new_long(3)),
(
"department".into(),
RestrictedExpression::new_string("Sales".into()),
),
(
"manager".into(),
RestrictedExpression::from_str(r#"Employee::"456""#).unwrap(),
),
("hr_contacts".into(), RestrictedExpression::new_set([])),
(
"json_blob".into(),
RestrictedExpression::new_record([
("inner1".into(), RestrictedExpression::new_bool(false)),
(
"inner2".into(),
RestrictedExpression::new_string("foo".into()),
),
(
"inner3".into(),
RestrictedExpression::new_record([(
"innerinner".into(),
RestrictedExpression::from_str(r#"Employee::"abc""#).unwrap(),
)])
.unwrap(),
),
])
.unwrap(),
),
(
"home_ip".into(),
RestrictedExpression::from_str(r#"ip("10.20.30.40")"#).unwrap(),
),
(
"work_ip".into(),
RestrictedExpression::from_str(r#"ip("10.50.60.70")"#).unwrap(),
),
(
"trust_score".into(),
RestrictedExpression::from_str(r#"decimal("36.53")"#).unwrap(),
),
(
"tricky".into(),
RestrictedExpression::from_str(r#"{ type: "foo", id: "bar" }"#).unwrap(),
),
]),
HashSet::new(),
)
.unwrap();
match validate_entity(entity, &schema) {
Ok(()) => panic!("expected an error due to extraneous attribute"),
Err(e) => {
assert!(
e.to_string().contains(r#"attribute `extra` on `Employee::"123"` should not exist according to the schema"#),
"actual error message was {e}",
);
}
}
let entity = Entity::new_no_attrs(EntityUid::from_strs("Manager", "jane"), HashSet::new());
match validate_entity(entity, &schema) {
Ok(()) => panic!("expected an error due to unexpected entity type"),
Err(e) => {
assert!(
e.to_string().contains(r#"entity `Manager::"jane"` has type `Manager` which is not declared in the schema"#),
"actual error message was {e}",
);
}
}
}
#[test]
fn issue_1176_should_fail1() {
let (schema, _) = Schema::from_cedarschema_str(
"
entity E {
rec: {
foo: Long
}
};
action Act appliesTo {
principal: [E],
resource: [E],
};
",
)
.unwrap();
let entity = Entity::new(
EntityUid::from_str(r#"E::"abc""#).unwrap(),
HashMap::from_iter([(
"rec".into(),
RestrictedExpression::new_record([
("foo".into(), RestrictedExpression::new_long(4567)),
(
"extra".into(),
RestrictedExpression::new_string("bad".into()),
),
])
.unwrap(),
)]),
HashSet::new(),
)
.unwrap();
assert_matches!(
Entities::from_entities([entity], Some(&schema)),
Err(EntitiesError::InvalidEntity(_))
);
}
#[test]
#[cfg(feature = "partial-validate")]
fn issue_1176_should_fail2() {
let schema = Schema::from_json_value(json!(
{
"": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"rec": {
"type": "Record",
"attributes": {
"foo": {
"type": "Long"
},
"bar": {
"type": "Boolean",
"required": false
}
},
"additionalAttributes": true
}
}
},
"memberOfTypes": []
}
},
"actions": {
"pull": {
"appliesTo": {
"principalTypes": [
"User"
],
"resourceTypes": [
"User"
]
}
}
}
}
}
))
.expect("should be a valid schema");
let entity = Entity::new(
EntityUid::from_str(r#"User::"abc""#).unwrap(),
HashMap::from_iter([(
"rec".into(),
RestrictedExpression::new_record([
("foo".into(), RestrictedExpression::new_long(4567)),
("bar".into(), RestrictedExpression::new_string("bad".into())),
])
.unwrap(),
)]),
HashSet::new(),
)
.unwrap();
assert_matches!(
Entities::from_entities([entity], Some(&schema)),
Err(EntitiesError::InvalidEntity(_))
);
}
#[test]
fn issue_1176_should_fail3() {
let (schema, _) = Schema::from_cedarschema_str(
r###"
entity A = {"foo": Set < Set < {"bar": __cedar::Bool, "baz"?: __cedar::Bool} > >};
action "g" appliesTo {
principal: [A],
resource: [A],
};
"###,
)
.unwrap();
let entity_str = r###"
{
"uid": {
"type": "A",
"id": "alice"
},
"attrs": {
"foo": [
[],
[
{
"bar": false
},
{
"bar": true
},
{
"bar": true,
"baz": true
}
],
[
{
"bar": false,
"baz": false
},
{
"bar": true
}
],
[
{
"bar": true
},
{
"baz": false
}
]
]
},
"parents": []
}
"###;
assert_matches!(Entity::from_json_str(entity_str, Some(&schema)), Err(_));
}
#[test]
fn should_pass_set_set_rec_one_req_one_opt() {
let (schema, _) = Schema::from_cedarschema_str(
r###"
entity A = {"foo": Set < Set < {"bar": __cedar::Bool, "baz"?: __cedar::Bool} > >};
action "g" appliesTo {
principal: [A],
resource: [A],
};
"###,
)
.unwrap();
let entity_str = r###"
{
"uid": {
"type": "A",
"id": "alice"
},
"attrs": {
"foo": [
[],
[
{
"bar": false
},
{
"bar": true
},
{
"bar": true,
"baz": true
}
],
[
{
"bar": false,
"baz": false
},
{
"bar": true
}
],
[
{
"bar": true
},
{
"bar": true,
"baz": false
}
]
]
},
"parents": []
}
"###;
assert_matches!(Entity::from_json_str(entity_str, Some(&schema)), Ok(_));
}
#[test]
fn example_app_tags() {
let (schema, _) = Schema::from_cedarschema_str(
r###"
entity User {
allowedTagsForRole: {
"Role-A"?: {
production_status?: Set<String>,
country?: Set<String>,
stage?: Set<String>,
},
"Role-B"?: {
production_status?: Set<String>,
country?: Set<String>,
stage?: Set<String>,
},
},
};
action UpdateWorkspace appliesTo {
principal: User,
resource: User,
};
"###,
)
.unwrap();
let entity_str = r###"
{
"uid": {
"type": "User",
"id": "Alice"
},
"attrs": {
"allowedTagsForRole": {
"Role-B": {
"production_status": [
"production"
],
"country": [
"ALL"
],
"stage": [
"valuation"
]
}
}
},
"parents": []
}
"###;
assert_matches!(Entity::from_json_str(entity_str, Some(&schema)), Ok(_));
}
#[test]
fn should_pass_set_set_record_one_req_one_opt() {
let (schema, _) = Schema::from_cedarschema_str(
r###"
entity A = {"qqamncWam": Set < Set < {"": __cedar::Bool, "bbrb"?: __cedar::Bool} > >};
action "g" appliesTo {
principal: [A],
resource: [A],
context: {"vlipwwpm0am": Set < Set < {"": __cedar::String, "b"?: __cedar::Bool} > >}
};
"###,
)
.unwrap();
let entity_str = r###"
{
"uid": {
"type": "A",
"id": ""
},
"attrs": {
"qqamncWam": [
[
{
"": false
},
{
"": false,
"bbrb": false
},
{
"": true
},
{
"": true,
"bbrb": false
},
{
"": true,
"bbrb": true
}
],
[
{
"": false
},
{
"": false,
"bbrb": true
},
{
"": true,
"bbrb": false
}
],
[
{
"": false,
"bbrb": false
},
{
"": false,
"bbrb": true
}
],
[
{
"": true
},
{
"": true,
"bbrb": true
}
],
[
{
"": true,
"bbrb": true
}
]
]
},
"parents": []
}
"###;
assert_matches!(Entity::from_json_str(entity_str, Some(&schema)), Ok(_));
}
}
mod schema_based_parsing_tests {
use super::*;
use cool_asserts::assert_matches;
use serde_json::json;
#[test]
fn entity_parse1() {
let e = r#"{
"uid" : { "type" : "User", "id" : "Alice" },
"attrs" : {},
"parents" : []
}"#;
let e = Entity::from_json_str(e, None).unwrap();
let (uid, attrs, parents) = e.into_inner();
let expected = r#"User::"Alice""#.parse().unwrap();
assert_eq!(uid, expected);
assert!(attrs.is_empty());
assert!(parents.is_empty());
}
#[test]
#[allow(clippy::too_many_lines)]
#[allow(clippy::cognitive_complexity)]
fn single_attr_types() {
let schema = Schema::from_json_value(json!(
{"": {
"entityTypes": {
"Employee": {
"memberOfTypes": [],
"shape": {
"type": "Record",
"attributes": {
"isFullTime": { "type": "Boolean" },
"numDirectReports": { "type": "Long" },
"department": { "type": "String" },
"manager": { "type": "Entity", "name": "Employee" },
"hr_contacts": { "type": "Set", "element": {
"type": "Entity", "name": "HR" } },
"json_blob": { "type": "Record", "attributes": {
"inner1": { "type": "Boolean" },
"inner2": { "type": "String" },
"inner3": { "type": "Record", "attributes": {
"innerinner": { "type": "Entity", "name": "Employee" }
}}
}},
"home_ip": { "type": "Extension", "name": "ipaddr" },
"work_ip": { "type": "Extension", "name": "ipaddr" },
"trust_score": { "type": "Extension", "name": "decimal" },
"tricky": { "type": "Record", "attributes": {
"type": { "type": "String" },
"id": { "type": "String" }
}}
}
}
},
"HR": {
"memberOfTypes": []
}
},
"actions": {
"view": { }
}
}}
))
.expect("should be a valid schema");
let entity = json!(
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
);
let parsed = Entity::from_json_value(entity.clone(), None).unwrap();
assert_matches!(
parsed.attr("home_ip"),
Some(Ok(EvalResult::String(s))) if &s == "222.222.222.101"
);
assert_matches!(
parsed.attr("trust_score"),
Some(Ok(EvalResult::String(s))) if &s == "5.7"
);
assert_matches!(parsed.attr("manager"), Some(Ok(EvalResult::Record(_))));
assert_matches!(parsed.attr("work_ip"), Some(Ok(EvalResult::Record(_))));
{
let Some(Ok(EvalResult::Set(set))) = parsed.attr("hr_contacts") else {
panic!("expected hr_contacts attr to exist and be a Set")
};
let contact = set.iter().next().expect("should be at least one contact");
assert_matches!(contact, EvalResult::Record(_));
};
{
let Some(Ok(EvalResult::Record(rec))) = parsed.attr("json_blob") else {
panic!("expected json_blob attr to exist and be a Record")
};
let inner3 = rec.get("inner3").expect("expected inner3 attr to exist");
let EvalResult::Record(rec) = inner3 else {
panic!("expected inner3 to be a Record")
};
let innerinner = rec
.get("innerinner")
.expect("expected innerinner attr to exist");
assert_matches!(innerinner, EvalResult::Record(_));
};
let parsed =
Entity::from_json_value(entity, Some(&schema)).expect("Should parse without error");
assert_matches!(parsed.attr("isFullTime"), Some(Ok(EvalResult::Bool(true))));
assert_matches!(
parsed.attr("numDirectReports"),
Some(Ok(EvalResult::Long(3)))
);
assert_matches!(
parsed.attr("department"),
Some(Ok(EvalResult::String(s))) if &s == "Sales"
);
assert_matches!(
parsed.attr("manager"),
Some(Ok(EvalResult::EntityUid(euid))) if euid == EntityUid::from_strs(
"Employee", "34FB87"
)
);
{
let Some(Ok(EvalResult::Set(set))) = parsed.attr("hr_contacts") else {
panic!("expected hr_contacts attr to exist and be a Set")
};
let contact = set.iter().next().expect("should be at least one contact");
assert_matches!(contact, EvalResult::EntityUid(_));
};
{
let Some(Ok(EvalResult::Record(rec))) = parsed.attr("json_blob") else {
panic!("expected json_blob attr to exist and be a Record")
};
let inner3 = rec.get("inner3").expect("expected inner3 attr to exist");
let EvalResult::Record(rec) = inner3 else {
panic!("expected inner3 to be a Record")
};
let innerinner = rec
.get("innerinner")
.expect("expected innerinner attr to exist");
assert_matches!(innerinner, EvalResult::EntityUid(_));
};
assert_matches!(
parsed.attr("home_ip"),
Some(Ok(EvalResult::ExtensionValue(ev))) if &ev == "222.222.222.101/32"
);
assert_matches!(
parsed.attr("work_ip"),
Some(Ok(EvalResult::ExtensionValue(ev))) if &ev == "2.2.2.0/24"
);
assert_matches!(
parsed.attr("trust_score"),
Some(Ok(EvalResult::ExtensionValue(ev))) if &ev == "5.7000"
);
let entity = json!(
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": "3",
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
);
let err = Entity::from_json_value(entity, Some(&schema))
.expect_err("should fail due to type mismatch on numDirectReports");
assert!(
err.to_string().contains(r#"in attribute `numDirectReports` on `Employee::"12UA45"`, type mismatch: value was expected to have type long, but actually has type string: `"3"`"#),
"actual error message was: `{err}`"
);
let entity = json!(
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": "34FB87",
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
);
let err = Entity::from_json_value(entity, Some(&schema))
.expect_err("should fail due to type mismatch on manager");
assert!(
err.to_string()
.contains(r#"in attribute `manager` on `Employee::"12UA45"`, expected a literal entity reference, but got `"34FB87"`"#),
"actual error message was {err}"
);
let entity = json!(
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": { "type": "HR", "id": "aaaaa" },
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
);
let err = Entity::from_json_value(entity, Some(&schema))
.expect_err("should fail due to type mismatch on hr_contacts");
assert!(
err.to_string().contains(r#"in attribute `hr_contacts` on `Employee::"12UA45"`, type mismatch: value was expected to have type (set of `HR`), but actually has type record with attributes: {"id" => (optional) string, "type" => (optional) string}: `{"id": "aaaaa", "type": "HR"}`"#),
"actual error message was {err}"
);
let entity = json!(
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "HR", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
);
let err = Entity::from_json_value(entity, Some(&schema))
.expect_err("should fail due to type mismatch on manager");
assert!(
err.to_string().contains(r#"in attribute `manager` on `Employee::"12UA45"`, type mismatch: value was expected to have type `Employee`, but actually has type `HR`: `HR::"34FB87"`"#),
"actual error message was {err}"
);
let entity = json!(
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": { "fn": "decimal", "arg": "3.33" },
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
);
let err = Entity::from_json_value(entity, Some(&schema))
.expect_err("should fail due to type mismatch on home_ip");
assert!(
err.to_string().contains(r#"in attribute `home_ip` on `Employee::"12UA45"`, type mismatch: value was expected to have type ipaddr, but actually has type decimal: `decimal("3.33")`"#),
"actual error message was {err}"
);
let entity = json!(
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
);
let err = Entity::from_json_value(entity, Some(&schema))
.expect_err("should fail due to missing attribute \"inner2\"");
assert!(
err.to_string().contains(r#"in attribute `json_blob` on `Employee::"12UA45"`, expected the record to have an attribute `inner2`, but it does not"#),
"actual error message was {err}"
);
let entity = json!(
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": 33,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
);
let err = Entity::from_json_value(entity, Some(&schema))
.expect_err("should fail due to type mismatch on attribute \"inner1\"");
assert!(
err.to_string().contains(r#"in attribute `json_blob` on `Employee::"12UA45"`, type mismatch: value was expected to have type record with attributes: "#),
"actual error message was {err}"
);
let entity = json!(
{
"uid": { "__entity": { "type": "Employee", "id": "12UA45" } },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "__entity": { "type": "Employee", "id": "34FB87" } },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": { "__extn": { "fn": "ip", "arg": "222.222.222.101" } },
"work_ip": { "__extn": { "fn": "ip", "arg": "2.2.2.0/24" } },
"trust_score": { "__extn": { "fn": "decimal", "arg": "5.7" } },
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
);
Entity::from_json_value(entity, Some(&schema))
.expect("this version with explicit __entity and __extn escapes should also pass");
}
#[test]
fn entity_fails_multiple() {
let json = json!(
[
{
"uid" : { "type" : "User", "id" : "Alice" },
"attrs" : {},
"parents" : []
},
{
"uid" : { "type" : "User", "id" : "Bob" },
"attrs" : {},
"parents" : []
},
]);
Entity::from_json_value(json, None).expect_err("Multiple entities should fail this parser");
let json = json!(
[
{
"uid" : { "type" : "User", "id" : "Alice" },
"attrs" : {},
"parents" : []
}
]);
Entity::from_json_value(json, None).expect_err("Multiple entities should fail this parser");
}
#[test]
#[allow(clippy::too_many_lines)]
#[allow(clippy::cognitive_complexity)]
fn attr_types() {
let schema = Schema::from_json_value(json!(
{"": {
"entityTypes": {
"Employee": {
"memberOfTypes": [],
"shape": {
"type": "Record",
"attributes": {
"isFullTime": { "type": "Boolean" },
"numDirectReports": { "type": "Long" },
"department": { "type": "String" },
"manager": { "type": "Entity", "name": "Employee" },
"hr_contacts": { "type": "Set", "element": {
"type": "Entity", "name": "HR" } },
"json_blob": { "type": "Record", "attributes": {
"inner1": { "type": "Boolean" },
"inner2": { "type": "String" },
"inner3": { "type": "Record", "attributes": {
"innerinner": { "type": "Entity", "name": "Employee" }
}}
}},
"home_ip": { "type": "Extension", "name": "ipaddr" },
"work_ip": { "type": "Extension", "name": "ipaddr" },
"trust_score": { "type": "Extension", "name": "decimal" },
"tricky": { "type": "Record", "attributes": {
"type": { "type": "String" },
"id": { "type": "String" }
}}
}
}
},
"HR": {
"memberOfTypes": []
}
},
"actions": {
"view": { }
}
}}
))
.expect("should be a valid schema");
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let parsed = Entities::from_json_value(entitiesjson.clone(), None)
.expect("Should parse without error");
assert_eq!(parsed.iter().count(), 1);
let parsed = parsed
.get(&EntityUid::from_strs("Employee", "12UA45"))
.expect("that should be the employee id");
assert_matches!(
parsed.attr("home_ip"),
Some(Ok(EvalResult::String(s))) if &s == "222.222.222.101"
);
assert_matches!(
parsed.attr("trust_score"),
Some(Ok(EvalResult::String(s))) if &s == "5.7"
);
assert_matches!(parsed.attr("manager"), Some(Ok(EvalResult::Record(_))));
assert_matches!(parsed.attr("work_ip"), Some(Ok(EvalResult::Record(_))));
{
let Some(Ok(EvalResult::Set(set))) = parsed.attr("hr_contacts") else {
panic!("expected hr_contacts attr to exist and be a Set")
};
let contact = set.iter().next().expect("should be at least one contact");
assert_matches!(contact, EvalResult::Record(_));
};
{
let Some(Ok(EvalResult::Record(rec))) = parsed.attr("json_blob") else {
panic!("expected json_blob attr to exist and be a Record")
};
let inner3 = rec.get("inner3").expect("expected inner3 attr to exist");
let EvalResult::Record(rec) = inner3 else {
panic!("expected inner3 to be a Record")
};
let innerinner = rec
.get("innerinner")
.expect("expected innerinner attr to exist");
assert_matches!(innerinner, EvalResult::Record(_));
};
let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
.expect("Should parse without error");
assert_eq!(parsed.iter().count(), 2); assert_eq!(
parsed
.iter()
.filter(|e| e.uid().type_name().basename() == "Action")
.count(),
1
);
let parsed = parsed
.get(&EntityUid::from_strs("Employee", "12UA45"))
.expect("that should be the employee id");
assert_matches!(parsed.attr("isFullTime"), Some(Ok(EvalResult::Bool(true))));
assert_matches!(
parsed.attr("numDirectReports"),
Some(Ok(EvalResult::Long(3)))
);
assert_matches!(
parsed.attr("department"),
Some(Ok(EvalResult::String(s))) if &s == "Sales"
);
assert_matches!(
parsed.attr("manager"),
Some(Ok(EvalResult::EntityUid(euid))) if euid == EntityUid::from_strs(
"Employee", "34FB87"
)
);
{
let Some(Ok(EvalResult::Set(set))) = parsed.attr("hr_contacts") else {
panic!("expected hr_contacts attr to exist and be a Set")
};
let contact = set.iter().next().expect("should be at least one contact");
assert_matches!(contact, EvalResult::EntityUid(_));
};
{
let Some(Ok(EvalResult::Record(rec))) = parsed.attr("json_blob") else {
panic!("expected json_blob attr to exist and be a Record")
};
let inner3 = rec.get("inner3").expect("expected inner3 attr to exist");
let EvalResult::Record(rec) = inner3 else {
panic!("expected inner3 to be a Record")
};
let innerinner = rec
.get("innerinner")
.expect("expected innerinner attr to exist");
assert_matches!(innerinner, EvalResult::EntityUid(_));
};
assert_matches!(
parsed.attr("home_ip"),
Some(Ok(EvalResult::ExtensionValue(ev))) if &ev == "222.222.222.101/32"
);
assert_matches!(
parsed.attr("work_ip"),
Some(Ok(EvalResult::ExtensionValue(ev))) if &ev == "2.2.2.0/24"
);
assert_matches!(
parsed.attr("trust_score"),
Some(Ok(EvalResult::ExtensionValue(ev))) if &ev == "5.7000"
);
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": "3",
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let err = Entities::from_json_value(entitiesjson, Some(&schema))
.expect_err("should fail due to type mismatch on numDirectReports");
assert!(
err.to_string().contains(r#"in attribute `numDirectReports` on `Employee::"12UA45"`, type mismatch: value was expected to have type long, but actually has type string: `"3"`"#),
"actual error message was {err}"
);
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": "34FB87",
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let err = Entities::from_json_value(entitiesjson, Some(&schema))
.expect_err("should fail due to type mismatch on manager");
assert!(
err.to_string()
.contains(r#"in attribute `manager` on `Employee::"12UA45"`, expected a literal entity reference, but got `"34FB87"`"#),
"actual error message was {err}"
);
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": { "type": "HR", "id": "aaaaa" },
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let err = Entities::from_json_value(entitiesjson, Some(&schema))
.expect_err("should fail due to type mismatch on hr_contacts");
assert!(
err.to_string().contains(r#"in attribute `hr_contacts` on `Employee::"12UA45"`, type mismatch: value was expected to have type (set of `HR`), but actually has type record with attributes: {"id" => (optional) string, "type" => (optional) string}: `{"id": "aaaaa", "type": "HR"}`"#),
"actual error message was {err}"
);
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "HR", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let err = Entities::from_json_value(entitiesjson, Some(&schema))
.expect_err("should fail due to type mismatch on manager");
assert!(
err.to_string().contains(r#"in attribute `manager` on `Employee::"12UA45"`, type mismatch: value was expected to have type `Employee`, but actually has type `HR`: `HR::"34FB87"`"#),
"actual error message was {err}"
);
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": { "fn": "decimal", "arg": "3.33" },
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let err = Entities::from_json_value(entitiesjson, Some(&schema))
.expect_err("should fail due to type mismatch on home_ip");
assert!(
err.to_string().contains(r#"in attribute `home_ip` on `Employee::"12UA45"`, type mismatch: value was expected to have type ipaddr, but actually has type decimal: `decimal("3.33")`"#),
"actual error message was {err}"
);
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let err = Entities::from_json_value(entitiesjson, Some(&schema))
.expect_err("should fail due to missing attribute \"inner2\"");
assert!(
err.to_string().contains(r#"in attribute `json_blob` on `Employee::"12UA45"`, expected the record to have an attribute `inner2`, but it does not"#),
"actual error message was {err}"
);
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": 33,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": "222.222.222.101",
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": "5.7",
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let err = Entities::from_json_value(entitiesjson, Some(&schema))
.expect_err("should fail due to type mismatch on attribute \"inner1\"");
assert!(
err.to_string().contains(r#"in attribute `json_blob` on `Employee::"12UA45"`, type mismatch: value was expected to have type record with attributes: "#),
"actual error message was {err}"
);
let entitiesjson = json!(
[
{
"uid": { "__entity": { "type": "Employee", "id": "12UA45" } },
"attrs": {
"isFullTime": true,
"numDirectReports": 3,
"department": "Sales",
"manager": { "__entity": { "type": "Employee", "id": "34FB87" } },
"hr_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "type": "HR", "id": "bbbbb" }
],
"json_blob": {
"inner1": false,
"inner2": "-*/",
"inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
},
"home_ip": { "__extn": { "fn": "ip", "arg": "222.222.222.101" } },
"work_ip": { "__extn": { "fn": "ip", "arg": "2.2.2.0/24" } },
"trust_score": { "__extn": { "fn": "decimal", "arg": "5.7" } },
"tricky": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
Entities::from_json_value(entitiesjson, Some(&schema))
.expect("this version with explicit __entity and __extn escapes should also pass");
}
#[test]
fn namespaces() {
let schema = Schema::from_str(
r#"
{"XYZCorp": {
"entityTypes": {
"Employee": {
"memberOfTypes": [],
"shape": {
"type": "Record",
"attributes": {
"isFullTime": { "type": "Boolean" },
"department": { "type": "String" },
"manager": {
"type": "Entity",
"name": "XYZCorp::Employee"
}
}
}
}
},
"actions": {
"view": {}
}
}}
"#,
)
.expect("should be a valid schema");
let entitiesjson = json!(
[
{
"uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"department": "Sales",
"manager": { "type": "XYZCorp::Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
.expect("Should parse without error");
assert_eq!(parsed.iter().count(), 2); assert_eq!(
parsed
.iter()
.filter(|e| e.uid().type_name().basename() == "Action")
.count(),
1
);
let parsed = parsed
.get(&EntityUid::from_strs("XYZCorp::Employee", "12UA45"))
.expect("that should be the employee type and id");
assert_matches!(parsed.attr("isFullTime"), Some(Ok(EvalResult::Bool(true))));
assert_matches!(
parsed.attr("department"),
Some(Ok(EvalResult::String(s))) if &s == "Sales"
);
assert_matches!(
parsed.attr("manager"),
Some(Ok(EvalResult::EntityUid(euid))) if euid == EntityUid::from_strs(
"XYZCorp::Employee",
"34FB87"
)
);
let entitiesjson = json!(
[
{
"uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let err = Entities::from_json_value(entitiesjson, Some(&schema))
.expect_err("should fail due to manager being wrong entity type (missing namespace)");
assert!(
err.to_string().contains(r#"in attribute `manager` on `XYZCorp::Employee::"12UA45"`, type mismatch: value was expected to have type `XYZCorp::Employee`, but actually has type `Employee`: `Employee::"34FB87"`"#),
"actual error message was {err}"
);
}
#[test]
fn optional_attrs() {
let schema = Schema::from_str(
r#"
{"": {
"entityTypes": {
"Employee": {
"memberOfTypes": [],
"shape": {
"type": "Record",
"attributes": {
"isFullTime": { "type": "Boolean" },
"department": { "type": "String", "required": false },
"manager": { "type": "Entity", "name": "Employee" }
}
}
}
},
"actions": {
"view": {}
}
}}
"#,
)
.expect("should be a valid schema");
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"department": "Sales",
"manager": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
.expect("Should parse without error");
assert_eq!(parsed.iter().count(), 2); assert_eq!(
parsed
.iter()
.filter(|e| e.uid().type_name().basename() == "Action")
.count(),
1
);
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"manager": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
.expect("Should parse without error");
assert_eq!(parsed.iter().count(), 2); assert_eq!(
parsed
.iter()
.filter(|e| e.uid().type_name().basename() == "Action")
.count(),
1
);
}
#[test]
fn schema_sanity_check() {
let src = "{ , .. }";
assert_matches!(Schema::from_str(src), Err(super::SchemaError::Serde(_)));
}
#[test]
fn template_constraint_sanity_checks() {
assert!(!TemplatePrincipalConstraint::Any.has_slot());
assert!(!TemplatePrincipalConstraint::In(Some(EntityUid::from_strs("a", "a"))).has_slot());
assert!(!TemplatePrincipalConstraint::Eq(Some(EntityUid::from_strs("a", "a"))).has_slot());
assert!(TemplatePrincipalConstraint::In(None).has_slot());
assert!(TemplatePrincipalConstraint::Eq(None).has_slot());
assert!(!TemplateResourceConstraint::Any.has_slot());
assert!(!TemplateResourceConstraint::In(Some(EntityUid::from_strs("a", "a"))).has_slot());
assert!(!TemplateResourceConstraint::Eq(Some(EntityUid::from_strs("a", "a"))).has_slot());
assert!(TemplateResourceConstraint::In(None).has_slot());
assert!(TemplateResourceConstraint::Eq(None).has_slot());
}
#[test]
fn template_principal_constraints() {
let src = r"
permit(principal == ?principal, action, resource);
";
let t = Template::parse(None, src).unwrap();
assert_eq!(
t.principal_constraint(),
TemplatePrincipalConstraint::Eq(None)
);
let src = r"
permit(principal in ?principal, action, resource);
";
let t = Template::parse(None, src).unwrap();
assert_eq!(
t.principal_constraint(),
TemplatePrincipalConstraint::In(None)
);
let src = r"
permit(principal is A in ?principal, action, resource);
";
let t = Template::parse(None, src).unwrap();
assert_eq!(
t.principal_constraint(),
TemplatePrincipalConstraint::IsIn(EntityTypeName::from_str("A").unwrap(), None)
);
}
#[test]
fn static_action_constraints() {
let src = r"
permit(principal, action, resource);
";
let p = Policy::parse(None, src).unwrap();
assert_eq!(p.action_constraint(), ActionConstraint::Any);
let src = r#"
permit(principal, action == Action::"A", resource);
"#;
let p = Policy::parse(None, src).unwrap();
assert_eq!(
p.action_constraint(),
ActionConstraint::Eq(EntityUid::from_strs("Action", "A"))
);
let src = r#"
permit(principal, action in [Action::"A", Action::"B"], resource);
"#;
let p = Policy::parse(None, src).unwrap();
assert_eq!(
p.action_constraint(),
ActionConstraint::In(vec![
EntityUid::from_strs("Action", "A"),
EntityUid::from_strs("Action", "B")
])
);
}
#[test]
fn template_resource_constraints() {
let src = r"
permit(principal, action, resource == ?resource);
";
let t = Template::parse(None, src).unwrap();
assert_eq!(
t.resource_constraint(),
TemplateResourceConstraint::Eq(None)
);
let src = r"
permit(principal, action, resource in ?resource);
";
let t = Template::parse(None, src).unwrap();
assert_eq!(
t.resource_constraint(),
TemplateResourceConstraint::In(None)
);
let src = r"
permit(principal, action, resource is A in ?resource);
";
let t = Template::parse(None, src).unwrap();
assert_eq!(
t.resource_constraint(),
TemplateResourceConstraint::IsIn(EntityTypeName::from_str("A").unwrap(), None)
);
}
#[test]
fn schema_namespace() {
let fragment: SchemaFragment = r#"
{
"Foo::Bar": {
"entityTypes": {},
"actions": {}
}
}
"#
.parse()
.unwrap();
let namespaces = fragment.namespaces().next().unwrap();
assert_eq!(
namespaces.map(|ns| ns.to_string()),
Some("Foo::Bar".to_string())
);
let _schema: Schema = fragment.try_into().expect("Should convert to schema");
let fragment: SchemaFragment = r#"
{
"": {
"entityTypes": {},
"actions": {}
}
}
"#
.parse()
.unwrap();
let namespaces = fragment.namespaces().next().unwrap();
assert_eq!(namespaces, None);
let _schema: Schema = fragment.try_into().expect("Should convert to schema");
}
#[test]
fn load_multiple_namespaces() {
let fragment = SchemaFragment::from_json_value(json!({
"Foo::Bar": {
"entityTypes": {
"Baz": {
"memberOfTypes": ["Bar::Foo::Baz"]
}
},
"actions": {}
},
"Bar::Foo": {
"entityTypes": {
"Baz": {
"memberOfTypes": ["Foo::Bar::Baz"]
}
},
"actions": {}
}
}))
.unwrap();
let schema = Schema::from_schema_fragments([fragment]).unwrap();
assert!(schema
.0
.get_entity_type(&"Foo::Bar::Baz".parse().unwrap())
.is_some());
assert!(schema
.0
.get_entity_type(&"Bar::Foo::Baz".parse().unwrap())
.is_some());
}
#[test]
fn get_attributes_from_schema() {
let fragment: SchemaFragment = SchemaFragment::from_json_value(json!({
"": {
"entityTypes": {},
"actions": {
"A": {},
"B": {
"memberOf": [{"id": "A"}]
},
"C": {
"memberOf": [{"id": "A"}]
},
"D": {
"memberOf": [{"id": "B"}, {"id": "C"}]
},
"E": {
"memberOf": [{"id": "D"}]
}
}
}}))
.unwrap();
let schema = Schema::from_schema_fragments([fragment]).unwrap();
let action_entities = schema.action_entities().unwrap();
let a_euid = EntityUid::from_strs("Action", "A");
let b_euid = EntityUid::from_strs("Action", "B");
let c_euid = EntityUid::from_strs("Action", "C");
let d_euid = EntityUid::from_strs("Action", "D");
let e_euid = EntityUid::from_strs("Action", "E");
assert_eq!(
action_entities,
Entities::from_entities(
[
Entity::new_no_attrs(a_euid.clone(), HashSet::new()),
Entity::new_no_attrs(b_euid.clone(), HashSet::from([a_euid.clone()])),
Entity::new_no_attrs(c_euid.clone(), HashSet::from([a_euid.clone()])),
Entity::new_no_attrs(
d_euid.clone(),
HashSet::from([a_euid.clone(), b_euid.clone(), c_euid.clone()])
),
Entity::new_no_attrs(e_euid, HashSet::from([a_euid, b_euid, c_euid, d_euid])),
],
Some(&schema)
)
.unwrap()
);
}
#[test]
fn entities_duplicates_fail() {
let json = serde_json::json!([
{
"uid" : {
"type" : "User",
"id" : "alice"
},
"attrs" : {},
"parents": []
},
{
"uid" : {
"type" : "User",
"id" : "alice"
},
"attrs" : {},
"parents": []
}
]);
let r = Entities::from_json_value(json, None).err().unwrap();
let expected_euid: cedar_policy_core::ast::EntityUID = r#"User::"alice""#.parse().unwrap();
match r {
EntitiesError::Duplicate(euid) => assert_eq!(euid, expected_euid),
e => panic!("Wrong error. Expected `Duplicate`, got: {e:?}"),
}
}
#[test]
fn issue_418() {
let schema = Schema::from_json_value(json!(
{"": {
"entityTypes": {
"Employee": {
"memberOfTypes": [],
"shape": {
"type": "Record",
"attributes": {
"isFullTime": { "type": "Boolean" },
"numDirectReports": { "type": "Long" },
"department": { "type": "String" },
"manager": { "type": "Entity", "name": "Employee" },
"hr_contacts": { "type": "Set", "element": {
"type": "Entity", "name": "HR" } },
"sales_contacts": { "type": "Set", "element": {
"type": "Entity", "name": "Employee" } },
"json_blob": { "type": "Record", "attributes": {
"inner1": { "type": "Boolean" },
"inner2": { "type": "String" },
"inner3": { "type": "Record", "attributes": {
"innerinner": { "type": "Entity", "name": "Employee" }
}}
}},
"home_ip": { "type": "Extension", "name": "ipaddr" },
"work_ip": { "type": "Extension", "name": "ipaddr" },
"trust_score": { "type": "Extension", "name": "decimal" },
"tricky": { "type": "Record", "attributes": {
"type": { "type": "String" },
"id": { "type": "String" }
}}
}
}
},
"HR": {
"memberOfTypes": []
}
},
"actions": {
"view": { }
}
}}
))
.expect("should be a valid schema");
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": { "__extn": { "fn": "unknown", "arg": "abc" }},
"numDirectReports": { "__extn": { "fn": "unknown", "arg": "def" }},
"department": { "__extn": { "fn": "unknown", "arg": "zxy" }},
"manager": { "__extn": { "fn": "unknown", "arg": "www" }},
"hr_contacts": { "__extn": { "fn": "unknown", "arg": "yyy" }},
"sales_contacts": [
{ "type": "HR", "id": "aaaaa" },
{ "__extn": { "fn": "unknown", "arg": "123" }}
],
"json_blob": {
"inner1": false,
"inner2": { "__extn": { "fn": "unknown", "arg": "hhh" }},
"inner3": { "innerinner": { "__extn": { "fn": "unknown", "arg": "bbb" }}},
},
"home_ip": { "__extn": { "fn": "unknown", "arg": "uuu" }},
"work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
"trust_score": { "__extn": { "fn": "unknown", "arg": "dec" }},
"tricky": { "__extn": { "fn": "unknown", "arg": "ttt" }}
},
"parents": []
}
]
);
let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
.expect("Should parse without error");
let parsed = parsed
.get(&EntityUid::from_strs("Employee", "12UA45"))
.expect("that should be the employee id");
let assert_contains_unknown = |err: &str, unk_name: &str| {
assert!(
err.contains("value contains a residual expression"),
"actual error message was {err}"
);
assert!(err.contains(unk_name), "actual error message was {err}");
};
assert_matches!(
parsed.attr("isFullTime"),
Some(Err(e)) => assert_contains_unknown(&e.to_string(), "abc")
);
assert_matches!(
parsed.attr("numDirectReports"),
Some(Err(e)) => assert_contains_unknown(&e.to_string(), "def")
);
assert_matches!(
parsed.attr("department"),
Some(Err(e)) => assert_contains_unknown(&e.to_string(), "zxy")
);
assert_matches!(
parsed.attr("manager"),
Some(Err(e)) => assert_contains_unknown(&e.to_string(), "www")
);
assert_matches!(
parsed.attr("hr_contacts"),
Some(Err(e)) => assert_contains_unknown(&e.to_string(), "yyy")
);
assert_matches!(
parsed.attr("sales_contacts"),
Some(Err(e)) => assert_contains_unknown(&e.to_string(), "123")
);
assert_matches!(
parsed.attr("json_blob"),
Some(Err(e)) => assert_contains_unknown(&e.to_string(), "bbb")
);
assert_matches!(
parsed.attr("home_ip"),
Some(Err(e)) => assert_contains_unknown(&e.to_string(), "uuu")
);
assert_matches!(parsed.attr("work_ip"), Some(Ok(_)));
assert_matches!(
parsed.attr("trust_score"),
Some(Err(e)) => assert_contains_unknown(&e.to_string(), "dec")
);
assert_matches!(
parsed.attr("tricky"),
Some(Err(e)) => assert_contains_unknown(&e.to_string(), "ttt")
);
}
#[test]
fn issue_285() {
let schema = Schema::from_json_value(json!(
{"": {
"entityTypes": {},
"actions": {
"A": {},
"B": {
"memberOf": [{"id": "A"}]
},
"C": {
"memberOf": [{"id": "B"}]
}
}
}}
))
.expect("should be a valid schema");
let entitiesjson_tc = json!(
[
{
"uid": { "type": "Action", "id": "A" },
"attrs": {},
"parents": []
},
{
"uid": { "type": "Action", "id": "B" },
"attrs": {},
"parents": [
{ "type": "Action", "id": "A" }
]
},
{
"uid": { "type": "Action", "id": "C" },
"attrs": {},
"parents": [
{ "type": "Action", "id": "A" },
{ "type": "Action", "id": "B" }
]
}
]
);
let entitiesjson_no_tc = json!(
[
{
"uid": { "type": "Action", "id": "A" },
"attrs": {},
"parents": []
},
{
"uid": { "type": "Action", "id": "B" },
"attrs": {},
"parents": [
{ "type": "Action", "id": "A" }
]
},
{
"uid": { "type": "Action", "id": "C" },
"attrs": {},
"parents": [
{ "type": "Action", "id": "B" }
]
}
]
);
assert!(Entities::from_json_value(entitiesjson_tc.clone(), Some(&schema)).is_ok());
assert!(Entities::from_json_value(entitiesjson_no_tc.clone(), Some(&schema)).is_ok());
let entitiesjson_bad = json!(
[
{
"uid": { "type": "Action", "id": "A" },
"attrs": {},
"parents": []
},
{
"uid": { "type": "Action", "id": "B" },
"attrs": {},
"parents": [
{ "type": "Action", "id": "A" }
]
},
{
"uid": { "type": "Action", "id": "C" },
"attrs": {},
"parents": [
{ "type": "Action", "id": "A" }
]
}
]
);
assert!(matches!(
Entities::from_json_value(entitiesjson_bad, Some(&schema)),
Err(EntitiesError::InvalidEntity(
entities::EntitySchemaConformanceError::ActionDeclarationMismatch { uid: _ }
))
));
let schema = cedar_policy_validator::CoreSchema::new(&schema.0);
let parser_assume_computed = entities::EntityJsonParser::new(
Some(&schema),
extensions::Extensions::all_available(),
entities::TCComputation::AssumeAlreadyComputed,
);
assert!(matches!(
parser_assume_computed.from_json_value(entitiesjson_no_tc.clone()),
Err(EntitiesError::InvalidEntity(
entities::EntitySchemaConformanceError::ActionDeclarationMismatch { uid: _ }
))
));
let parser_enforce_computed = entities::EntityJsonParser::new(
Some(&schema),
extensions::Extensions::all_available(),
entities::TCComputation::EnforceAlreadyComputed,
);
assert!(matches!(
parser_enforce_computed.from_json_value(entitiesjson_no_tc),
Err(EntitiesError::TransitiveClosureError(_))
));
}
}
#[cfg(not(feature = "partial-validate"))]
#[test]
fn partial_schema_unsupported() {
use cool_asserts::assert_panics;
use serde_json::json;
assert_panics!(
Schema::from_json_value( json!({"": { "entityTypes": { "A": { "shape": { "type": "Record", "attributes": {}, "additionalAttributes": true } } }, "actions": {} }})).unwrap(),
includes("records and entities with `additionalAttributes` are experimental, but the experimental `partial-validate` feature is not enabled")
);
}
#[cfg(feature = "partial-validate")]
mod partial_schema {
use super::*;
use serde_json::json;
fn partial_schema() -> Schema {
Schema::from_json_value(json!(
{
"": {
"entityTypes": {
"Employee": {
"memberOfTypes": [],
"shape": {
"type": "Record",
"attributes": { },
"additionalAttributes": true,
},
}
},
"actions": {
"Act": {
"appliesTo": {
"context": {
"type": "Record",
"attributes": {},
"additionalAttributes": true,
}
}
}
}
}
}
))
.unwrap()
}
#[test]
fn entity_extra_attr() {
let entitiesjson = json!(
[
{
"uid": { "type": "Employee", "id": "12UA45" },
"attrs": {
"isFullTime": true,
"foobar": 234,
"manager": { "type": "Employee", "id": "34FB87" }
},
"parents": []
}
]
);
let schema = partial_schema();
let parsed = Entities::from_json_value(entitiesjson.clone(), Some(&schema))
.expect("Parsing with a partial schema should allow unknown attributes.");
let parsed_without_schema = Entities::from_json_value(entitiesjson, None).unwrap();
let uid = EntityUid::from_strs("Employee", "12UA45");
assert_eq!(
parsed.get(&uid),
parsed_without_schema.get(&uid),
"Parsing with a partial schema should give the same result as parsing without a schema"
);
}
#[test]
fn context_extra_attr() {
Context::from_json_value(
json!({"foo": true, "bar": 123}),
Some((&partial_schema(), &EntityUid::from_strs("Action", "Act"))),
)
.unwrap();
}
}
mod template_tests {
use std::str::FromStr;
use crate::Template;
use cedar_policy_core::test_utils::*;
#[test]
fn test_policy_template_to_json() {
let template = Template::parse(
None,
"permit(principal == ?principal, action, resource in ?resource);",
);
assert_eq!(
template.unwrap().to_json().unwrap().to_string(),
r#"{"effect":"permit","principal":{"op":"==","slot":"?principal"},"action":{"op":"All"},"resource":{"op":"in","slot":"?resource"},"conditions":[]}"#
);
}
#[test]
fn test_policy_template_from_json() {
let template = Template::from_json(None, serde_json::from_str(r#"{"effect":"permit","principal":{"op":"==","slot":"?principal"},"action":{"op":"All"},"resource":{"op":"in","slot":"?resource"},"conditions":[]}"#).unwrap());
assert_eq!(
template.unwrap().to_string(),
"permit(principal == ?principal, action, resource in ?resource);".to_string()
);
}
#[track_caller]
fn assert_not_a_template(src: &str) {
let e = Template::from_str(src).unwrap_err();
expect_err(
src,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("expected a template, got a static policy")
.help("a template should include slot(s) `?principal` or `?resource`")
.exactly_one_underline(src)
.build(),
)
}
#[test]
fn test_static_policy_as_template() {
assert_not_a_template("permit(principal == User::\"alice\", action, resource is Photo);");
assert_not_a_template("permit(principal,action,resource);");
assert_not_a_template("permit(principal == T::\"a\",action,resource);");
assert_not_a_template("permit(principal in T::\"a\",action,resource);");
assert_not_a_template("permit(principal is T in T::\"a\",action,resource);");
assert_not_a_template("permit(principal is T,action,resource);");
assert_not_a_template("permit(principal,action == Action::\"a\",resource);");
assert_not_a_template(
"permit(principal,action in [Action::\"a\",Action::\"b\"],resource);",
);
assert_not_a_template("permit(principal,action,resource == T::\"a\");");
assert_not_a_template("permit(principal,action,resource in T::\"a\");");
assert_not_a_template("permit(principal,action,resource is T in T::\"a\");");
assert_not_a_template("permit(principal,action,resource is T);");
}
}
mod issue_326 {
#[test]
fn shows_only_the_first_parse_error_in_display() {
use crate::PolicySet;
use cool_asserts::assert_matches;
use itertools::Itertools;
use miette::Diagnostic;
use std::str::FromStr;
let src = r"
permit (
principal
action
resource
);
";
assert_matches!(PolicySet::from_str(src), Err(e) => {
assert!(e.to_string().contains("unexpected token `action`"), "actual error message was {e}");
assert!(!e.to_string().contains("unexpected token `resource`"), "actual error message was {e}");
assert!(
e.related().into_iter().flatten().any(|err| err.to_string().contains("unexpected token `resource`")),
"actual related error messages were\n{}",
e.related().into_iter().flatten().map(ToString::to_string).join("\n")
);
});
}
}
mod policy_id_tests {
use super::*;
#[test]
fn test_default_policy_id() {
let policy = crate::Policy::from_str(r"permit(principal, action, resource);")
.expect("should succeed");
let policy_id: &str = policy.id().as_ref();
assert_eq!(policy_id, "policy0");
}
}
mod error_source_tests {
use super::*;
use cool_asserts::assert_matches;
use miette::Diagnostic;
use serde_json::json;
#[test]
fn errors_have_source_location_and_source_code() {
let srcs = [
r#"@one("two") @one("three") permit(principal, action, resource);"#,
r#"superforbid ( principal in Group::"bad", action, resource );"#,
r#"permit ( principal is User::"alice", action, resource );"#,
];
for src in srcs {
assert_matches!(PolicySet::from_str(src), Err(e) => {
assert!(e.labels().is_some(), "no source span for the parse error resulting from:\n {src}\nerror was:\n{:?}", miette::Report::new(e));
assert!(e.source_code().is_some(), "no source code for the parse error resulting from:\n {src}\nerror was:\n{:?}", miette::Report::new(e));
});
}
let srcs = [
"1 + true",
"3 has foo",
"true && ([2, 3, 4] in [4, 5, 6])",
"ip(3)",
];
let req = Request::new(None, None, None, Context::empty(), None).unwrap();
let entities = Entities::empty();
for src in srcs {
let expr = Expression::from_str(src).unwrap();
assert_matches!(eval_expression(&req, &entities, &expr), Err(_e) => {
});
}
let srcs = [
"permit ( principal, action, resource ) when { 1 + true };",
"permit ( principal, action, resource ) when { 3 has foo };",
"permit ( principal, action, resource ) when { true && ([2, 3, 4] in [4, 5, 6]) };",
"permit ( principal, action, resource ) when { ip(3) };",
];
let req = Request::new(None, None, None, Context::empty(), None).unwrap();
let entities = Entities::empty();
for src in srcs {
let pset = PolicySet::from_str(src).unwrap();
let resp = Authorizer::new().is_authorized(&req, &pset, &entities);
for _err in resp.diagnostics().errors() {
}
}
let validator = Validator::new(
Schema::from_json_value(json!({ "": { "actions": { "view": {} }, "entityTypes": {} }}))
.unwrap(),
);
for src in srcs {
let pset = PolicySet::from_str(src).unwrap();
let res = validator.validate(&pset, ValidationMode::Strict);
for err in res.validation_errors() {
assert!(err.labels().is_some(), "no source span for the validation error resulting from:\n {src}\nerror was:\n{:?}", miette::Report::new(err.clone()));
assert!(err.source_code().is_some(), "no source code for the validation error resulting from:\n {src}\nerror was:\n{:?}", miette::Report::new(err.clone()));
}
for warn in res.validation_warnings() {
assert!(warn.labels().is_some(), "no source span for the validation error resulting from:\n {src}\nerror was:\n{:?}", miette::Report::new(warn.clone()));
assert!(warn.source_code().is_some(), "no source code for the validation error resulting from:\n {src}\nerror was:\n{:?}", miette::Report::new(warn.clone()));
}
}
}
}
mod issue_604 {
use crate::Policy;
use cedar_policy_core::parser::parse_policy_or_template_to_est;
use cool_asserts::assert_matches;
#[track_caller]
fn to_json_is_ok(text: &str) {
let policy = Policy::parse(None, text).unwrap();
let json = policy.to_json();
assert_matches!(json, Ok(_));
}
#[track_caller]
fn make_policy_with_get_attr(attr: &str) -> String {
format!(
r#"
permit(principal, action, resource) when {{ principal == resource.{attr} }};
"#
)
}
#[track_caller]
fn make_policy_with_has_attr(attr: &str) -> String {
format!(
r#"
permit(principal, action, resource) when {{ resource has {attr} }};
"#
)
}
#[test]
fn var_as_attribute_name() {
for attr in ["principal", "action", "resource", "context"] {
to_json_is_ok(&make_policy_with_get_attr(attr));
to_json_is_ok(&make_policy_with_has_attr(attr));
}
}
#[track_caller]
fn is_valid_est(text: &str) {
let est = parse_policy_or_template_to_est(text);
assert_matches!(est, Ok(_));
}
#[track_caller]
fn is_invalid_est(text: &str) {
let est = parse_policy_or_template_to_est(text);
assert_matches!(est, Err(_));
}
#[test]
fn keyword_as_attribute_name_err() {
for attr in ["true", "false", "if", "then", "else", "in", "like", "has"] {
is_invalid_est(&make_policy_with_get_attr(attr));
is_invalid_est(&make_policy_with_has_attr(attr));
}
}
#[test]
fn keyword_as_attribute_name_ok() {
for attr in ["permit", "forbid", "when", "unless", "_"] {
is_valid_est(&make_policy_with_get_attr(attr));
is_valid_est(&make_policy_with_has_attr(attr));
}
}
}
mod issue_606 {
use cedar_policy_core::est::FromJsonError;
use crate::{PolicyId, Template};
#[test]
fn est_template() {
let est_json = serde_json::json!({
"effect": "permit",
"principal": { "op": "All" },
"action": { "op": "All" },
"resource": { "op": "All" },
"conditions": [
{
"kind": "when",
"body": {
"==": {
"left": { "Var": "principal" },
"right": { "Slot": "?principal" }
}
}
}
]
});
let tid = PolicyId::new("t0");
let template = Template::from_json(Some(tid.clone()), est_json);
assert!(matches!(
template,
Err(FromJsonError::SlotsInConditionClause {
slot: _,
clausetype: "when"
})
));
}
}
mod issue_619 {
use crate::{eval_expression, Context, Entities, EvalResult, Policy, Request};
use cool_asserts::assert_matches;
#[test]
fn issue_619() {
let policy = Policy::parse(
None,
r#"permit(principal, action, resource) when {1 * 2 * true};"#,
)
.unwrap();
let json = policy.to_json().unwrap();
let _ = Policy::from_json(None, json).unwrap();
}
#[test]
fn mult_overflows() {
let eval = |expr: &str| {
eval_expression(
&Request::new(None, None, None, Context::empty(), None).unwrap(),
&Entities::empty(),
&expr.parse().unwrap(),
)
};
assert_matches!(eval(&format!("{}*{}*0", 1_i64 << 62, 1_i64 << 62)), Err(e) => {
assert_eq!(&e.to_string(), "integer overflow while attempting to multiply the values `4611686018427387904` and `4611686018427387904`");
});
assert_matches!(
eval(&format!("{}*0*{}", 1_i64 << 62, 1_i64 << 62)),
Ok(EvalResult::Long(0))
);
assert_matches!(
eval(&format!("0*{}*{}", 1_i64 << 62, 1_i64 << 62)),
Ok(EvalResult::Long(0))
);
}
}
mod into_iter_entities {
use super::*;
use smol_str::SmolStr;
#[test]
fn into_iter_entities() {
let test_data = r#"
[
{
"uid": {"type":"User","id":"alice"},
"attrs": {
"age":19,
"ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
},
"parents": [{"type":"Group","id":"admin"}]
},
{
"uid": {"type":"Group","id":"admin"},
"attrs": {},
"parents": []
}
]
"#;
let list = Entities::from_json_str(test_data, None).unwrap();
let mut list_out: Vec<SmolStr> = list
.into_iter()
.map(|entity| entity.uid().id().escaped())
.collect();
list_out.sort();
assert_eq!(list_out, &["admin", "alice"]);
}
}
mod context_tests {
use cool_asserts::assert_matches;
use serde_json::json;
use super::*;
#[test]
fn merge_contexts() {
let context_pt_1 = Context::from_json_value(json!({"key1": "foo", "key2": true}), None)
.expect("context creation should have succeeded");
let pairs = vec![(String::from("key3"), RestrictedExpression::new_long(42))];
let context_pt_2 =
Context::from_pairs(pairs).expect("context creation should have succeeded");
let context = context_pt_1
.merge(context_pt_2)
.expect("context merge should have succeeded");
let values = context.into_iter();
for (k, v) in values {
match k.as_ref() {
"key1" => {
assert_matches!(
v.into_inner().expr_kind(),
ast::ExprKind::Lit(ast::Literal::String(s)) => {
assert_eq!(s.as_str(), "foo");
}
);
}
"key2" => {
assert_matches!(
v.into_inner().expr_kind(),
ast::ExprKind::Lit(ast::Literal::Bool(true)),
);
}
"key3" => {
assert_matches!(
v.into_inner().expr_kind(),
ast::ExprKind::Lit(ast::Literal::Long(42)),
);
}
_ => {
panic!("unexpected key `{k}`");
}
}
}
}
#[test]
fn merge_contexts_duplicate_keys() {
let context_pt_1 = Context::from_json_value(json!({"key1": "foo", "key2": true}), None)
.expect("context creation should have succeeded");
let pairs = vec![(String::from("key2"), RestrictedExpression::new_long(42))];
let context_pt_2 =
Context::from_pairs(pairs).expect("context creation should have succeeded");
let err = context_pt_1.merge(context_pt_2).unwrap_err();
assert_eq!(err.to_string(), "duplicate key `key2` in context");
}
}