Skip to content

Commit

Permalink
human-readable test fixtures (#10006)
Browse files Browse the repository at this point in the history
* feat(jans-cedarling): Encoding and ContentType for cedar_schema and policy_content values

Signed-off-by: John Anderson <[email protected]>

* feat(jans-cedarling): deserialize from schema field with metadata in policy.json

Signed-off-by: John Anderson <[email protected]>

* feat(jans-cedarling): deserialize from policy_content field with metadata in policy.json

Signed-off-by: John Anderson <[email protected]>

* feat(jans-cedarling): Ensure that policies are only ever encoded in cedar, because parsing cedar-json is currently not handled by cedar-policy crate.

Signed-off-by: John Anderson <[email protected]>

* feat(jans-cedarling): for very human-readable tests, you can now do test file fixtures in yaml

Signed-off-by: John Anderson <[email protected]>

* feat(jans-cedarling): rectify clippy complaints

Signed-off-by: John Anderson <[email protected]>

* feat(jans-cedarling): local use for std::collections::HashSet

Signed-off-by: John Anderson <[email protected]>

---------

Signed-off-by: John Anderson <[email protected]>
  • Loading branch information
djellemah authored and rmarinn committed Nov 3, 2024
1 parent e98bde6 commit 274db44
Show file tree
Hide file tree
Showing 7 changed files with 395 additions and 73 deletions.
1 change: 1 addition & 0 deletions jans-cedarling/cedarling/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ test_utils = { workspace = true }
rand = "0.8.5"
jsonwebkey = { version = "0.3.5", features = ["generate", "jwt-convert"] }
mockito = "1.5.0"
serde_yml = "0.0.12"
232 changes: 180 additions & 52 deletions jans-cedarling/cedarling/src/common/cedar_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,154 @@
*/

pub(crate) use cedar_json::CedarSchemaJson;

pub(crate) mod cedar_json;

/// cedar_schema value which specifies both encoding and content_type
///
/// encoding is one of none or base64
/// content_type is one of cedar or cedar-json#[derive(Debug, Clone, serde::Deserialize)]
#[derive(Debug, Clone, serde::Deserialize)]
struct EncodedSchema {
pub encoding : super::Encoding,
pub content_type : super::ContentType,
pub body : String,
}

/// Intermediate struct to handle both kinds of cedar_schema values.
///
/// Either
/// "cedar_schema": "cGVybWl0KA..."
/// OR
/// "cedar_schema": { "encoding": "...", "content_type": "...", "body": "permit(...)"}#[derive(Debug, Clone, serde::Deserialize)]
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(untagged)]
enum MaybeEncoded {
Plain(String),
Tagged(EncodedSchema)
}

/// Box that holds the [`cedar_policy::Schema`] and
/// JSON representation that is used to create entities from the schema in the policy store.
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(crate) struct CedarSchema {
pub schema: cedar_policy::Schema,
pub json: cedar_json::CedarSchemaJson,
}

impl PartialEq for CedarSchema {
fn eq(&self, other: &Self) -> bool {
// Have to check principals, resources, action_groups, entity_types,
// actions. Those can contain duplicates, and are not stored in comparison order.
// So use HashSet to compare them.
use std::collections::HashSet;

let self_principals = self.schema.principals().collect::<HashSet<_>>();
let other_principals = other.schema.principals().collect::<HashSet<_>>();
if self_principals != other_principals {
return false
}

let self_resources = self.schema.resources().collect::<HashSet<_>>();
let other_resources = other.schema.resources().collect::<HashSet<_>>();
if self_resources != other_resources {
return false
}

let self_action_groups = self.schema.action_groups().collect::<HashSet<_>>();
let other_action_groups = other.schema.action_groups().collect::<HashSet<_>>();
if self_action_groups != other_action_groups {
return false
}

let self_entity_types = self.schema.entity_types().collect::<HashSet<_>>();
let other_entity_types = other.schema.entity_types().collect::<HashSet<_>>();
if self_entity_types != other_entity_types {
return false
}

let self_actions = self.schema.actions().collect::<HashSet<_>>();
let other_actions = other.schema.actions().collect::<HashSet<_>>();
if self_actions != other_actions {
return false
}

// and this only checks the schema anyway
self.json == other.json
}
}

impl<'de> serde::Deserialize<'de> for CedarSchema {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
fn deserialize<D : serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error>
{
deserialize::parse_cedar_schema(deserializer)
// Read the next thing as either a String or a Map, using the MaybeEncoded enum to distinguish
let encoded_schema = match <MaybeEncoded as serde::Deserialize>::deserialize(deserializer)? {
MaybeEncoded::Plain(body) => EncodedSchema{
// These are the default if the encoding is not specified.
encoding: super::Encoding::Base64,
content_type: super::ContentType::CedarJson,
body
},
MaybeEncoded::Tagged(encoded_schema) => encoded_schema,
};

let decoded_body = match encoded_schema.encoding {
super::Encoding::None => encoded_schema.body,
super::Encoding::Base64 => {
use base64::prelude::*;
let buf = BASE64_STANDARD.decode(encoded_schema.body).map_err(|err| {
serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::Base64, err))
})?;
String::from_utf8(buf).map_err(|err| {
serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::Utf8, err))
})?
}
};

// Need both of these because CedarSchema wants both.
let (schema_fragment, json_string) = match encoded_schema.content_type {
super::ContentType::Cedar => {
// parse cedar policy from the cedar representation
// TODO must log warnings or something
let (schema_fragment, _warning) = cedar_policy::SchemaFragment::from_cedarschema_str(&decoded_body)
.map_err(|err| {
serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::Parse, err))
})?;

// urgh now recreate the json representation
let json_string = schema_fragment.to_json_string()
.map_err(|err| {
serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::CedarSchemaJsonFormat, err))
})?;

(schema_fragment, json_string)
}
super::ContentType::CedarJson => {
// parse cedar policy from the json representation
let schema_fragment = cedar_policy::SchemaFragment::from_json_str(&decoded_body)
.map_err(|err| {
serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::CedarSchemaJsonFormat, err))
})?;
(schema_fragment, decoded_body)
}
};

// create the schema
let fragment_iter = std::iter::once(schema_fragment);
let schema = cedar_policy::Schema::from_schema_fragments(fragment_iter.into_iter())
.map_err(|err| {
serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::Parse, err))
})?;

let json = serde_json::from_str(&json_string)
.map_err(|err| {
serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::CedarSchemaJsonFormat, err ))
})?;

Ok(CedarSchema{schema, json})
}
}

mod deserialize {
use super::*;
use base64::prelude::*;

#[derive(Debug, thiserror::Error)]
pub enum ParseCedarSchemaSetMessage {
#[error("unable to decode cedar policy schema base64")]
Expand All @@ -39,40 +162,8 @@ mod deserialize {
CedarSchemaJsonFormat,
#[error("unable to parse cedar policy schema json")]
Parse,
}

/// A custom deserializer for Cedar's Schema.
//
// is used to deserialize field `cedar_schema` in `PolicyStore` from base64 and get [`cedar_policy::Schema`]
pub(crate) fn parse_cedar_schema<'de, D>(deserializer: D) -> Result<CedarSchema, D::Error>
where
D: serde::Deserializer<'de>,
{
let source = <String as serde::Deserialize>::deserialize(deserializer)?;
let decoded: Vec<u8> = BASE64_STANDARD.decode(source.as_str()).map_err(|err| {
serde::de::Error::custom(format!("{}: {}", ParseCedarSchemaSetMessage::Base64, err,))
})?;

// parse cedar policy schema to the our structure
let cedar_policy_json: CedarSchemaJson = serde_json::from_reader(decoded.as_slice())
.map_err(|err| {
serde::de::Error::custom(format!(
"{}: {}",
ParseCedarSchemaSetMessage::CedarSchemaJsonFormat,
err
))
})?;

// parse cedar policy schema to the `cedar_policy::Schema`
let cedar_policy_schema = cedar_policy::Schema::from_json_file(decoded.as_slice())
.map_err(|err| {
serde::de::Error::custom(format!("{}: {}", ParseCedarSchemaSetMessage::Parse, err))
})?;

Ok(CedarSchema {
schema: cedar_policy_schema,
json: cedar_policy_json,
})
#[error("invalid utf8 detected while decoding cedar policy")]
Utf8,
}

#[cfg(test)]
Expand All @@ -89,7 +180,46 @@ mod deserialize {
include_str!("../../../test_files/policy-store_ok.json");

let policy_result = serde_json::from_str::<PolicyStore>(POLICY_STORE_RAW);
assert!(policy_result.is_ok());
assert!(policy_result.is_ok(), "{:?}", policy_result.unwrap_err());
}

#[test]
fn test_readable_ok() {
static POLICY_STORE_RAW: &str =
include_str!("../../../test_files/policy-store_readable.json");

let policy_result = serde_json::from_str::<PolicyStore>(POLICY_STORE_RAW);
assert!(policy_result.is_ok(), "{:?}", policy_result.unwrap_err());
}

#[test]
fn test_readable_yaml_ok() {
static YAML_POLICY_STORE: &str = include_str!("../../../test_files/policy-store_readable.yaml");
let yaml_policy_result = serde_yml::from_str::<PolicyStore>(YAML_POLICY_STORE);
assert!(yaml_policy_result.is_ok(), "{:?}", yaml_policy_result.unwrap_err());
}

#[test]
fn test_readable_yaml_identical_readable_json() {
static YAML_POLICY_STORE: &str = include_str!("../../../test_files/policy-store_readable.yaml");
let yaml_policy_result = serde_yml::from_str::<PolicyStore>(YAML_POLICY_STORE);

static JSON_POLICY_STORE: &str = include_str!("../../../test_files/policy-store_readable.json");
let json_policy_result = serde_yml::from_str::<PolicyStore>(JSON_POLICY_STORE);

assert_eq!(yaml_policy_result.unwrap(), json_policy_result.unwrap());
}

// In fact this fails because of limitations in cedar_policy::Policy::from_json
// see PolicyContentType
fn test_both_ok() {
static POLICY_STORE_RAW: &str =
include_str!("../../../test_files/policy-store_blobby.json");

let policy_result = serde_json::from_str::<PolicyStore>(POLICY_STORE_RAW);
let err = policy_result.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("data did not match any variant of untagged enum MaybeEncoded"));
}

#[test]
Expand All @@ -98,10 +228,9 @@ mod deserialize {
include_str!("../../../test_files/policy-store_schema_err_base64.json");

let policy_result = serde_json::from_str::<PolicyStore>(POLICY_STORE_RAW);
assert!(policy_result
.unwrap_err()
.to_string()
.contains(&ParseCedarSchemaSetMessage::Base64.to_string()));
let err = policy_result.unwrap_err();
let msg = err.to_string();
assert!(msg.contains(&ParseCedarSchemaSetMessage::Base64.to_string()), "{err:?}");
}

#[test]
Expand All @@ -110,10 +239,9 @@ mod deserialize {
include_str!("../../../test_files/policy-store_schema_err_json.json");

let policy_result = serde_json::from_str::<PolicyStore>(POLICY_STORE_RAW);
assert!(policy_result
.unwrap_err()
.to_string()
.contains(&ParseCedarSchemaSetMessage::CedarSchemaJsonFormat.to_string()));
let err = policy_result.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("unable to unmarshal cedar policy schema json"), "{err:?}");
}

#[test]
Expand Down
24 changes: 24 additions & 0 deletions jans-cedarling/cedarling/src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,27 @@ pub(crate) mod app_types;
pub(crate) mod cedar_schema;

pub mod policy_store;

/// Used for decoding the policy and schema metadata
#[derive(Debug, Clone, serde::Deserialize)]
enum Encoding {
/// indicates that the related value is base64 encoded
#[serde(rename = "base64")]
Base64,

/// indicates that the related value is not encoded, ie it's just a plain string
#[serde(rename = "none")]
None,
}

/// Used for decoding the policy and schema metadata
#[derive(Debug, Clone, serde::Deserialize)]
enum ContentType {
/// indicates that the related value is in the cedar policy / schema language
#[serde(rename = "cedar")]
Cedar,

/// indicates that the related value is in the json representation of the cedar policy / schema language
#[serde(rename = "cedar-json")]
CedarJson
}
Loading

0 comments on commit 274db44

Please sign in to comment.