Skip to content

Commit

Permalink
feat(jans-cedarling): implement YAML parser for single policy store
Browse files Browse the repository at this point in the history
- Simplify YAML test files by removing the need for
  a top-level `policy_store` ID
- Ensure YAML test files exclusively contain human-readable Cedar code;
  base64-encoded schemas are now only used for JSON test files.
- Pending: Replace the existing implementation with the new parser.

Signed-off-by: rmarinn <[email protected]>
  • Loading branch information
rmarinn committed Nov 11, 2024
1 parent 69d902c commit fcae8e9
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 53 deletions.
51 changes: 51 additions & 0 deletions jans-cedarling/cedarling/src/common/policy_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,3 +397,54 @@ where

Ok(policy)
}

/// Custom deserializer for converting base64-encoded policies into a `PolicySet`.
///
/// This function is used to deserialize the `policies` field in `PolicyStore`.
#[allow(dead_code)]
pub fn parse_cedar_policy_new<'de, D>(deserializer: D) -> Result<cedar_policy::PolicySet, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[allow(dead_code)]
struct RawPolicy {
description: String,
creation_date: String,
policy_content: String,
}

let policies = <HashMap<String, RawPolicy> as serde::Deserialize>::deserialize(deserializer)?;

let results: Vec<Result<cedar_policy::Policy, D::Error>> = policies
.into_iter()
.map(|(id, policy_raw)| {
println!("parsing: {:?}", id);
let policy_id = Some(PolicyId::new(id));
cedar_policy::Policy::parse(policy_id, policy_raw.policy_content)
.map_err(serde::de::Error::custom)
})
.collect();

let (successful_policies, errors): (Vec<_>, Vec<_>) =
results.into_iter().partition(Result::is_ok);

// Collect all errors into a single error message or return them as a vector.
if !errors.is_empty() {
let error_messages: Vec<D::Error> = errors.into_iter().filter_map(Result::err).collect();

return Err(serde::de::Error::custom(format!(
"Errors encountered while parsing cedar policies: {:?}",
error_messages
)));
}

let policy_vec = successful_policies
.into_iter()
.filter_map(Result::ok)
.collect::<Vec<_>>();

cedar_policy::PolicySet::from_policies(policy_vec).map_err(|err| {
serde::de::Error::custom(format!("{}: {err}", ParsePolicySetMessage::CreatePolicySet))
})
}
Original file line number Diff line number Diff line change
@@ -1,54 +1,42 @@
cedar_version: v4.0.0
policy_stores:
a1bf93115de86de760ee0bea1d529b521489e5a11747:
cedar_version: v4.0.0
name: PolicyStoreOk
description: A test policy store where everything is fine.
policies:
840da5d85403f35ea76519ed1a18a33989f855bf1cf8:
description: simple policy example for principal workload
creation_date: '2024-09-20T17:22:39.996050'
policy_content:
encoding: none
content_type: cedar
body: |-
permit(
principal is Jans::Workload,
action in [Jans::Action::"Update"],
resource is Jans::Issue
)when{
principal.org_id == resource.org_id
};
444da5d85403f35ea76519ed1a18a33989f855bf1cf8:
description: simple policy example for principal user
creation_date: '2024-09-20T17:22:39.996050'
policy_content:
encoding: none
content_type: cedar
body: |-
permit(
principal is Jans::User,
action in [Jans::Action::"Update"],
resource is Jans::Issue
)when{
principal.country == resource.country
};
schema:
encoding: none
content_type: cedar
body: |-
namespace Jans {
type Url = {"host": String, "path": String, "protocol": String};
entity Access_token = {"aud": String, "exp": Long, "iat": Long, "iss": TrustedIssuer, "jti": String};
entity Issue = {"country": String, "org_id": String};
entity Role;
entity TrustedIssuer = {"issuer_entity_id": Url};
entity User in [Role] = {"country": String, "email": String, "sub": String, "username": String};
entity Workload = {"client_id": String, "iss": TrustedIssuer, "name": String, "org_id": String};
entity id_token = {"acr": String, "amr": String, "aud": String, "exp": Long, "iat": Long, "iss": TrustedIssuer, "jti": String, "sub": String};
action "Update" appliesTo {
principal: [Workload, User, Role],
resource: [Issue],
context: {}
};
}
name: PolicyStoreOk
description: A test policy store where everything is fine.
policies:
"some_policy_id":
description: simple policy example for principal workload
creation_date: '2024-09-20T17:22:39.996050'
policy_content: |-
permit(
principal is Jans::Workload,
action in [Jans::Action::"Update"],
resource is Jans::Issue
)when{
principal.org_id == resource.org_id
};
"another_policy_id":
description: simple policy example for principal user
creation_date: '2024-09-20T17:22:39.996050'
policy_content: |-
permit(
principal is Jans::User,
action in [Jans::Action::"Update"],
resource is Jans::Issue
)when{
principal.country == resource.country
};
schema: |-
namespace Jans {
type Url = {"host": String, "path": String, "protocol": String};
entity Access_token = {"aud": String, "exp": Long, "iat": Long, "iss": TrustedIssuer, "jti": String};
entity Issue = {"country": String, "org_id": String};
entity Role;
entity TrustedIssuer = {"issuer_entity_id": Url};
entity User in [Role] = {"country": String, "email": String, "sub": String, "username": String};
entity Workload = {"client_id": String, "iss": TrustedIssuer, "name": String, "org_id": String};
entity id_token = {"acr": String, "amr": String, "aud": String, "exp": Long, "iat": Long, "iss": TrustedIssuer, "jti": String, "sub": String};
action "Update" appliesTo {
principal: [Workload, User, Role],
resource: [Issue],
context: {}
};
}
57 changes: 57 additions & 0 deletions jans-cedarling/cedarling/src/common/policy_store/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use super::ParsePolicySetMessage;
use super::PolicyStore;
use crate::common::policy_store::parse_cedar_version;
use base64::prelude::*;
use cedar_policy::Policy;
use cedar_policy::PolicyId;
use serde::Deserialize;
use serde_json::json;
use std::str::FromStr;
use test_utils::assert_eq;
Expand Down Expand Up @@ -189,3 +192,57 @@ fn test_invalid_version_format_with_v() {
let invalid_version_with_v = "v1.2".to_string();
assert!(parse_cedar_version(serde_json::Value::String(invalid_version_with_v)).is_err());
}

#[test]
fn test_parsing_policy_store_from_yaml() {
use super::parse_cedar_policy_new;

let expected_policies = Vec::from([
Policy::parse(
Some(PolicyId::new("some_policy_id")),
r#"permit(
principal is Jans::Workload,
action in [Jans::Action::"Update"],
resource is Jans::Issue
)when{
principal.org_id == resource.org_id
};"#,
)
.expect("Should parse Cedar policy."),
Policy::parse(
Some(PolicyId::new("another_policy_id")),
r#"permit(
principal is Jans::User,
action in [Jans::Action::"Update"],
resource is Jans::Issue
)when{
principal.country == resource.country
};"#,
)
.expect("Should parse Cedar policy."),
]);

#[derive(Debug, Deserialize)]
struct TestPolicyStore {
#[serde(alias = "policies", deserialize_with = "parse_cedar_policy_new")]
pub cedar_policies: cedar_policy::PolicySet,
}

static POLICY_STORE_RAW_YAML: &str = include_str!("./policy-store_ok.yaml");
let parsed = serde_yml::from_str::<TestPolicyStore>(POLICY_STORE_RAW_YAML)
.expect("should parse policy store from YAML");
let parsed_policies = parsed
.cedar_policies
.policies()
.cloned()
.collect::<Vec<Policy>>();

// Check that all expected policies are found in the parsed set
for policy in &expected_policies {
assert!(
parsed_policies.contains(policy),
"Expected to find policy in the parsed policies: {:?}",
policy.id()
);
}
}

0 comments on commit fcae8e9

Please sign in to comment.