Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion cedar-policy-core/src/parser/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ extern {
type Error = RawUserError;
}

// New tokens should be reflected in the `FRIENDLY_TOKEN_NAMES` map in err.rs.
match {
// Whitespace and comments
r"\s*" => { }, // The default whitespace skipping is disabled an `ignore pattern` is specified
Expand Down
2 changes: 2 additions & 0 deletions cedar-policy-validator/src/cedar_schema/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ pub struct EntityDecl {
pub member_of_types: Vec<Path>,
/// Attributes this entity has
pub attrs: Vec<Node<AttrDecl>>,
/// Tag type for this entity (`None` means no tags on this entity)
pub tags: Option<Node<Type>>,
}

/// Type definitions
Expand Down
6 changes: 3 additions & 3 deletions cedar-policy-validator/src/cedar_schema/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ extern {
type Error = RawUserError;
}

// New tokens should be reflected in the `FRIENDLY_TOKEN_NAMES` map in err.rs.
match {
// Whitespace and comments
r"\s*" => { }, // The default whitespace skipping is disabled an `ignore pattern` is specified
Expand All @@ -62,6 +61,7 @@ match {
"entity" => ENTITY,
"in" => IN,
"type" => TYPE,
"tags" => TAGS,
"Set" => SET,
"appliesTo" => APPLIESTO,
"principal" => PRINCIPAL,
Expand Down Expand Up @@ -108,8 +108,8 @@ Decl: Node<Declaration> = {

// Entity := 'entity' Idents ['in' EntOrTypes] [['='] RecType] ';'
Entity: Node<Declaration> = {
<l:@L> ENTITY <ets: Idents> <ps:(IN <EntTypes>)?> <ds:("="? "{" <AttrDecls?> "}")?> ";" <r:@R>
=> Node::with_source_loc(Declaration::Entity(EntityDecl { names: ets, member_of_types: ps.unwrap_or_default(), attrs: ds.map(|ds| ds.unwrap_or_default()).unwrap_or_default()}), Loc::new(l..r, Arc::clone(src))),
<l:@L> ENTITY <ets: Idents> <ps:(IN <EntTypes>)?> <ds:("="? "{" <AttrDecls?> "}")?> <ts:(TAGS <Type>)?> ";" <r:@R>
=> Node::with_source_loc(Declaration::Entity(EntityDecl { names: ets, member_of_types: ps.unwrap_or_default(), attrs: ds.map(|ds| ds.unwrap_or_default()).unwrap_or_default(), tags: ts }), Loc::new(l..r, Arc::clone(src))),
}

// Action := 'action' Names ['in' QualNameOrNames]
Expand Down
72 changes: 72 additions & 0 deletions cedar-policy-validator/src/cedar_schema/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ namespace Baz {action "Foo" appliesTo {
json_schema::EntityType::<RawName> {
member_of_types: vec![],
shape: json_schema::AttributesOrContext::default(),
tags: None,
},
)]),
actions: HashMap::from([(
Expand Down Expand Up @@ -2296,3 +2297,74 @@ mod common_type_references {
);
}
}

/// Tests involving entity tags (RFC 82)
#[cfg(test)]
mod entity_tags {
use crate::json_schema;
use crate::schema::test::collect_warnings;
use cedar_policy_core::extensions::Extensions;
use cool_asserts::assert_matches;

#[test]
fn basic_examples() {
let src = "entity E;";
assert_matches!(collect_warnings(json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available())), Ok((frag, warnings)) => {
assert!(warnings.is_empty());
let entity_type = frag.0.get(&None).unwrap().entity_types.get(&"E".parse().unwrap()).unwrap();
assert_matches!(&entity_type.tags, None);
});

let src = "entity E tags String;";
assert_matches!(collect_warnings(json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available())), Ok((frag, warnings)) => {
assert!(warnings.is_empty());
let entity_type = frag.0.get(&None).unwrap().entity_types.get(&"E".parse().unwrap()).unwrap();
assert_matches!(&entity_type.tags, Some(json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name })) => {
assert_eq!(&format!("{type_name}"), "String");
});
});

let src = "entity E tags Set<String>;";
assert_matches!(collect_warnings(json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available())), Ok((frag, warnings)) => {
assert!(warnings.is_empty());
let entity_type = frag.0.get(&None).unwrap().entity_types.get(&"E".parse().unwrap()).unwrap();
assert_matches!(&entity_type.tags, Some(json_schema::Type::Type(json_schema::TypeVariant::Set { element })) => {
assert_matches!(&**element, json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name }) => {
assert_eq!(&format!("{type_name}"), "String");
});
});
});

let src = "entity E { foo: String } tags { foo: String };";
assert_matches!(collect_warnings(json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available())), Ok((frag, warnings)) => {
assert!(warnings.is_empty());
let entity_type = frag.0.get(&None).unwrap().entity_types.get(&"E".parse().unwrap()).unwrap();
assert_matches!(&entity_type.tags, Some(json_schema::Type::Type(json_schema::TypeVariant::Record(rty))) => {
assert_matches!(rty.attributes.get("foo"), Some(json_schema::TypeOfAttribute { ty, required }) => {
assert_matches!(ty, json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name }) => {
assert_eq!(&format!("{type_name}"), "String");
});
assert_eq!(*required, true);
});
});
});

let src = "type T = String; entity E tags T;";
assert_matches!(collect_warnings(json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available())), Ok((frag, warnings)) => {
assert!(warnings.is_empty());
let entity_type = frag.0.get(&None).unwrap().entity_types.get(&"E".parse().unwrap()).unwrap();
assert_matches!(&entity_type.tags, Some(json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name })) => {
assert_eq!(&format!("{type_name}"), "T");
});
});

let src = "entity E tags E;";
assert_matches!(collect_warnings(json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available())), Ok((frag, warnings)) => {
assert!(warnings.is_empty());
let entity_type = frag.0.get(&None).unwrap().entity_types.get(&"E".parse().unwrap()).unwrap();
assert_matches!(&entity_type.tags, Some(json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name })) => {
assert_eq!(&format!("{type_name}"), "E");
});
});
}
}
1 change: 1 addition & 0 deletions cedar-policy-validator/src/cedar_schema/to_json_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ fn convert_entity_decl(
let etype = json_schema::EntityType {
member_of_types: e.member_of_types.into_iter().map(RawName::from).collect(),
shape: convert_attr_decls(e.attrs),
tags: e.tags.map(|tag_ty| cedar_type_to_json_type(tag_ty)),
};

// Then map over all of the bound names
Expand Down
157 changes: 157 additions & 0 deletions cedar-policy-validator/src/json_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,10 @@ pub struct EntityType<N> {
#[serde(default)]
#[serde(skip_serializing_if = "AttributesOrContext::is_empty_record")]
pub shape: AttributesOrContext<N>,
/// Tag type for entities of this [`EntityType`]; `None` means entities of this [`EntityType`] do not have tags.
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Type<N>>,
}

impl EntityType<RawName> {
Expand All @@ -409,6 +413,9 @@ impl EntityType<RawName> {
.map(|rname| rname.conditionally_qualify_with(ns, ReferenceType::Entity)) // Only entity, not common, here for now; see #1064
.collect(),
shape: self.shape.conditionally_qualify_type_references(ns),
tags: self
.tags
.map(|ty| ty.conditionally_qualify_type_references(ns)),
}
}
}
Expand All @@ -431,6 +438,10 @@ impl EntityType<ConditionalName> {
.map(|cname| cname.resolve(all_defs))
.collect::<std::result::Result<_, _>>()?,
shape: self.shape.fully_qualify_type_references(all_defs)?,
tags: self
.tags
.map(|ty| ty.fully_qualify_type_references(all_defs))
.transpose()?,
})
}
}
Expand Down Expand Up @@ -2433,6 +2444,150 @@ mod strengthened_types {
}
}

/// Tests involving entity tags (RFC 82)
#[cfg(test)]
mod entity_tags {
use super::*;
use cedar_policy_core::test_utils::{expect_err, ExpectedErrorMessageBuilder};
use cool_asserts::assert_matches;
use serde_json::json;

/// This schema taken directly from the RFC 82 text
#[test]
fn basic() {
let json = json!({"": {
"entityTypes": {
"User" : {
"shape" : {
"type" : "Record",
"attributes" : {
"jobLevel" : {
"type" : "Long"
},
}
},
"tags" : {
"type" : "Set",
"element": { "type": "String" }
}
},
"Document" : {
"shape" : {
"type" : "Record",
"attributes" : {
"owner" : {
"type" : "Entity",
"name" : "User"
},
}
},
"tags" : {
"type" : "Set",
"element": { "type": "String" }
}
}
},
"actions": {}
}});
assert_matches!(Fragment::from_json_value(json), Ok(frag) => {
let user = frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap();
assert_matches!(&user.tags, Some(Type::Type(TypeVariant::Set { element })) => {
assert_matches!(&**element, Type::Type(TypeVariant::String)); // TODO: why is this `TypeVariant::String` in this case but `EntityOrCommon { "String" }` in all the other cases in this test? Do we accept common types as the element type for sets?
});
let doc = frag.0.get(&None).unwrap().entity_types.get(&"Document".parse().unwrap()).unwrap();
assert_matches!(&doc.tags, Some(Type::Type(TypeVariant::Set { element })) => {
assert_matches!(&**element, Type::Type(TypeVariant::String)); // TODO: why is this `TypeVariant::String` in this case but `EntityOrCommon { "String" }` in all the other cases in this test? Do we accept common types as the element type for sets?
});
})
}

/// In this schema, the tag type is a common type
#[test]
fn tag_type_is_common_type() {
let json = json!({"": {
"commonTypes": {
"T": { "type": "String" },
},
"entityTypes": {
"User" : {
"shape" : {
"type" : "Record",
"attributes" : {
"jobLevel" : {
"type" : "Long"
},
}
},
"tags" : { "type" : "T" },
},
},
"actions": {}
}});
assert_matches!(Fragment::from_json_value(json), Ok(frag) => {
let user = frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap();
assert_matches!(&user.tags, Some(Type::CommonTypeRef { type_name }) => {
assert_eq!(&format!("{type_name}"), "T");
});
})
}

/// In this schema, the tag type is an entity type
#[test]
fn tag_type_is_entity_type() {
let json = json!({"": {
"entityTypes": {
"User" : {
"shape" : {
"type" : "Record",
"attributes" : {
"jobLevel" : {
"type" : "Long"
},
}
},
"tags" : { "type" : "Entity", "name": "User" },
},
},
"actions": {}
}});
assert_matches!(Fragment::from_json_value(json), Ok(frag) => {
let user = frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap();
assert_matches!(&user.tags, Some(Type::Type(TypeVariant::Entity{ name })) => {
assert_eq!(&format!("{name}"), "User");
});
})
}

/// This schema has `tags` inside `shape` instead of parallel to it
#[test]
fn bad_tags() {
let json = json!({"": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"jobLevel": {
"type": "Long"
},
},
"tags": { "type": "String" },
}
},
},
"actions": {}
}});
assert_matches!(Fragment::from_json_value(json.clone()), Err(e) => {
expect_err(
&json,
&miette::Report::new(e),
&ExpectedErrorMessageBuilder::error("unknown field `tags`, expected one of `type`, `element`, `attributes`, `additionalAttributes`, `name`")
.build(),
);
});
}
}

/// Check that (de)serialization works as expected.
#[cfg(test)]
mod test_json_roundtrip {
Expand Down Expand Up @@ -2485,6 +2640,7 @@ mod test_json_roundtrip {
attributes: BTreeMap::new(),
additional_attributes: false,
}))),
tags: None,
},
)]),
actions: HashMap::from([(
Expand Down Expand Up @@ -2526,6 +2682,7 @@ mod test_json_roundtrip {
additional_attributes: false,
},
))),
tags: None,
},
)]),
actions: HashMap::new(),
Expand Down
2 changes: 2 additions & 0 deletions cedar-policy-validator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,15 @@ mod test {
json_schema::EntityType {
member_of_types: vec![],
shape: json_schema::AttributesOrContext::default(),
tags: None,
},
),
(
bar_type.parse().unwrap(),
json_schema::EntityType {
member_of_types: vec![],
shape: json_schema::AttributesOrContext::default(),
tags: None,
},
),
],
Expand Down
Loading
Loading