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
239 changes: 239 additions & 0 deletions cedar-policy-core/src/entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1332,6 +1332,245 @@ mod json_parsing_tests {
});
}

/// Test that `null` is properly rejected, with a sane error message, in
/// various positions
#[test]
fn null_failures() {
let eparser: EntityJsonParser<'_, '_> =
EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);

let json = serde_json::json!(
[
{
"uid": null,
"attrs": {},
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error_and_help(
"in uid field of <unknown entity>, expected a literal entity reference, but got `null`",
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
));
});

let json = serde_json::json!(
[
{
"uid": { "type": null, "id": "bar" },
"attrs": {},
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error_and_help(
r#"in uid field of <unknown entity>, expected a literal entity reference, but got `{"id":"bar","type":null}`"#,
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
));
});

let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": null },
"attrs": {},
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error_and_help(
r#"in uid field of <unknown entity>, expected a literal entity reference, but got `{"id":null,"type":"foo"}`"#,
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
));
});

let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": null,
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error(
"invalid type: null, expected a map"
));
});

let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": null },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error(
r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
));
});

let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": { "subattr": null } },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error(
r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
));
});

let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": [ 3, null ] },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error(
r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
));
});

let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": [ 3, { "subattr" : null } ] },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error(
r#"in attribute `attr` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
));
});

let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "__extn": { "fn": null, "args": [] } },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error(
r#"in attribute `__extn` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
));
});

let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "__extn": { "fn": "ip", "args": null } },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error(
r#"in attribute `__extn` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
));
});

let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "__extn": { "fn": "ip", "args": [ null ] } },
"parents": [],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error(
r#"in attribute `__extn` on `foo::"bar"`, found a `null`; JSON `null`s are not allowed in Cedar"#,
));
});

let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": 2 },
"parents": null,
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error(
"invalid type: null, expected a sequence"
));
});

let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": 2 },
"parents": [ null ],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error_and_help(
r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `null`"#,
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
));
});

let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": 2 },
"parents": [ { "type": "foo", "id": null } ],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error_and_help(
r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `{"id":null,"type":"foo"}`"#,
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
));
});

let json = serde_json::json!(
[
{
"uid": { "type": "foo", "id": "bar" },
"attrs": { "attr": 2 },
"parents": [ { "type": "foo", "id": "parent" }, null ],
}
]
);
assert_matches!(eparser.from_json_value(json.clone()), Err(EntitiesError::Deserialization(e)) => {
expect_err(&json, &e, &ExpectedErrorMessage::error_and_help(
r#"in parents field of `foo::"bar"`, expected a literal entity reference, but got `null`"#,
r#"literal entity references can be made with `{ "type": "SomeType", "id": "SomeId" }`"#,
));
});
}

/// helper function to round-trip an Entities (with no schema-based parsing)
fn roundtrip(entities: &Entities) -> Result<Entities> {
let mut buf = Vec::new();
Expand Down
3 changes: 3 additions & 0 deletions cedar-policy-core/src/entities/json/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ pub enum JsonDeserializationError {
#[error("{0}, the `__expr` escape is no longer supported")]
#[diagnostic(help("to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"))]
ExprTag(Box<JsonDeserializationErrorContext>),
/// Raised when the input JSON contains a `null`
#[error("{0}, found a `null`; JSON `null`s are not allowed in Cedar")]
Null(Box<JsonDeserializationErrorContext>),
}

/// Errors thrown during serialization to JSON
Expand Down
4 changes: 4 additions & 0 deletions cedar-policy-core/src/entities/json/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ pub enum CedarValueJson {
Record(
#[cfg_attr(feature = "wasm", tsify(type = "{ [key: string]: CedarValueJson }"))] JsonRecord,
),
/// JSON null, which is never valid, but we put this here in order to
/// provide a better error message.
Null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this just implicitly deserailize from null without additional annotation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does seem so, as shown by this PR's tests

}

/// Structure representing a Cedar record in JSON
Expand Down Expand Up @@ -258,6 +261,7 @@ impl CedarValueJson {
)),
Self::ExtnEscape { __extn: extn } => extn.into_expr(ctx),
Self::ExprEscape { .. } => Err(JsonDeserializationError::ExprTag(Box::new(ctx()))),
Self::Null => Err(JsonDeserializationError::Null(Box::new(ctx()))),
}
}

Expand Down
2 changes: 1 addition & 1 deletion cedar-policy-core/src/est.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2239,7 +2239,7 @@ mod test {
let policy = r#"
permit(principal, action, resource)
when {

"" like "ḛ̶͑͝x̶͔͛a̵̰̯͛m̴͉̋́p̷̠͂l̵͇̍̔ȩ̶̣͝"
};
"#;
Expand Down
4 changes: 4 additions & 0 deletions cedar-policy-core/src/est/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1483,6 +1483,10 @@ fn display_cedarvaluejson(f: &mut std::fmt::Formatter<'_>, v: &CedarValueJson) -
write!(f, "}}")?;
Ok(())
}
CedarValueJson::Null => {
write!(f, "null")?;
Ok(())
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions cedar-policy-validator/src/schema/namespace_def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@ impl ValidatorNamespaceDef {
"extension function escape (`__extn`)".to_owned(),
))
}
CedarValueJson::Null => Err(SchemaError::UnsupportedActionAttribute(
action_id.clone(),
"null".to_owned(),
)),
}
}

Expand Down