cedarling 0.0.39

The Cedarling: a high-performance local authorization service powered by the Rust Cedar Engine.
Documentation
// This software is available under the Apache-2.0 license.
// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text.
//
// Copyright (c) 2024, Gluu, Inc.

use std::str::FromStr;

use base64::prelude::*;
use serde_json::json;

use super::{LegacyAgamaPolicyStore, LegacyPolicyStore, ParsePolicySetMessage};

/// Tests successful deserialization of a valid policy store JSON.
#[test]
fn test_policy_store_deserialization_success() {
    let policy = r#"
        permit (
            principal is Jans::Workload,
            action in [Jans::Action::"Update"],
            resource is Jans::Issue
        ) when {
            principal.org_id == resource.org_id
        };
    "#;
    cedar_policy::Policy::from_str(policy).expect("invalid cedar policy");

    let schema = include_str!("../cedar-schema.json");
    cedar_policy::Schema::from_json_str(schema).expect("invalid cedar schema");

    let policy_store_json = json!({
        "cedar_version": "v4.0.0",
        "name": "Jans",
        "cedar_policies": {
            "840da5d85403f35ea76519ed1a18a33989f855bf1cf8": {
                "description": "simple policy example",
                "creation_date": "2024-09-20T17:22:39.996050",
                "policy_content": BASE64_STANDARD.encode(policy)
            }
        },
        "cedar_schema": BASE64_STANDARD.encode(schema),
    });

    serde_json::from_str::<LegacyPolicyStore>(policy_store_json.to_string().as_str())
        .expect("failed to deserialize LegacyPolicyStore from policy_store_json");
}

#[test]
fn test_base64_decoding_error_in_policy_store() {
    let policy = r#"
        permit (
            principal is Jans::Workload,
            action in [Jans::Action::"Update"],
            resource is Jans::Issue
        ) when {
            principal.org_id == resource.org_id
        };
    "#;
    cedar_policy::Policy::from_str(policy).expect("invalid cedar policy");
    let mut encoded_policy = BASE64_STANDARD.encode(policy);
    encoded_policy.push('!');

    let schema = include_str!("../cedar-schema.json");
    cedar_policy::Schema::from_json_str(schema).expect("invalid cedar schema");

    let policy_store_json = json!({
        "cedar_version": "v4.0.0",
        "name": "Jans",
        "cedar_policies": {
            "840da5d85403f35ea76519ed1a18a33989f855bf1cf8": {
                "description": "simple policy example",
                "creation_date": "2024-09-20T17:22:39.996050",
                "policy_content": encoded_policy,
            }
        },
        "cedar_schema": BASE64_STANDARD.encode(schema),
    });

    let policy_result =
        serde_json::from_str::<LegacyPolicyStore>(policy_store_json.to_string().as_str());
    let err =
        policy_result.expect_err("Expected base64 decoding error for invalid base64 character");
    assert!(
        err.to_string()
            .contains(&ParsePolicySetMessage::Base64.to_string()),
        "Error message should indicate base64 decoding failure, got: {err}"
    );
}

#[test]
fn test_policy_parsing_error_in_policy_store() {
    let policy = r#"
        permit (
            principal is Jans::Workload,
            action in [Jans::Action::"Update"],
            resource is Jans::Issue
        ) when {
            principal.org_id == resource.org_id
        };
    "#;
    cedar_policy::Policy::from_str(policy).expect("invalid cedar policy");

    let mut encoded_policy = BASE64_STANDARD.encode(policy);
    let mut invalid_utf8_bytes = BASE64_STANDARD
        .decode(&encoded_policy)
        .expect("Failed to decode Base64");
    invalid_utf8_bytes[10] = 0xFF;
    encoded_policy = BASE64_STANDARD.encode(&invalid_utf8_bytes);

    let schema = include_str!("../cedar-schema.json");
    cedar_policy::Schema::from_json_str(schema).expect("invalid cedar schema");

    let policy_store_json = json!({
        "cedar_version": "v4.0.0",
        "name": "Jans",
        "cedar_policies": {
            "840da5d85403f35ea76519ed1a18a33989f855bf1cf8": {
                "description": "simple policy example",
                "creation_date": "2024-09-20T17:22:39.996050",
                "policy_content": encoded_policy,
            }
        },
        "cedar_schema": BASE64_STANDARD.encode(schema),
    });

    let policy_result =
        serde_json::from_str::<LegacyPolicyStore>(policy_store_json.to_string().as_str());
    let err = policy_result.expect_err("Expected UTF-8 parsing error for invalid byte sequence");
    assert!(
        err.to_string()
            .contains(&ParsePolicySetMessage::String.to_string()),
        "Error message should indicate string parsing failure, got: {err}"
    );
}

#[test]
fn test_broken_policy_parsing_error_in_policy_store() {
    static POLICY_STORE_RAW_YAML: &str =
        include_str!("../../../../../test_files/policy-store_policy_err_broken_policy.yaml");

    let policy_result = serde_yaml_ng::from_str::<LegacyAgamaPolicyStore>(POLICY_STORE_RAW_YAML);
    let err = policy_result.expect_err("Expected policy parsing error for broken policy syntax");
    let err_msg = err.to_string();

    assert!(
        err_msg.contains(
            "unable to decode policy with id: 840da5d85403f35ea76519ed1a18a33989f855bf1cf8"
        ),
        "Error should identify the policy ID that failed to decode, got: {err_msg}"
    );
    assert!(
        err_msg.contains(
            "unable to decode policy_content from human readable format: this policy is missing the `resource` variable in the scope"
        ),
        "Error should describe the syntax error, got: {err_msg}"
    );
}

#[test]
fn test_invalid_policy_store_entry() {
    let json = json!({
        "cedar_version": "v4.0.0",
        "policy_stores": {
            "test": {
                "name": "test",
                "policies": {}
            }
        }
    });

    let result = serde_json::from_str::<LegacyAgamaPolicyStore>(&json.to_string());
    result.expect("schema is now optional, should succeed without schema field");

    let json = json!({
        "cedar_version": "v4.0.0",
        "policy_stores": {
            "test": {
                "name": "test",
                "schema": null,
            }
        }
    });

    let result = serde_json::from_str::<LegacyAgamaPolicyStore>(&json.to_string());
    let err = result.expect_err("Expected error for missing policies in policy store entry");
    assert!(
        err.to_string().contains(
            "missing required field 'policies' or 'cedar_policies' in policy store entry"
        ),
        "Error should mention missing policies field, got: {err}"
    );
}

#[test]
fn test_invalid_schema_format() {
    let json = json!({
        "cedar_version": "v4.0.0",
        "policy_stores": {
            "test": {
                "name": "test",
                "schema": "invalid_schema",
                "policies": {}
            }
        }
    });

    let result = serde_json::from_str::<LegacyAgamaPolicyStore>(&json.to_string());
    let err = result.expect_err("Expected error for invalid schema format");
    assert!(
        err.to_string().contains("error parsing schema"),
        "Error should mention schema parsing error, got: {err}"
    );
}

#[test]
fn test_invalid_policies_format() {
    let schema = base64::prelude::BASE64_STANDARD.encode("{}");
    let json = json!({
        "cedar_version": "v4.0.0",
        "policy_stores": {
            "test": {
                "name": "test",
                "schema": schema,
                "policies": {
                    "invalid_policy": {
                        "description": "test",
                        "policy_content": "invalid_content"
                    }
                }
            }
        }
    });

    let result = serde_json::from_str::<LegacyAgamaPolicyStore>(&json.to_string());
    let err = result.expect_err("Expected error for invalid policy content");
    assert!(
        err.to_string().contains("unable to decode policy with id"),
        "Error should mention unable to decode policy, got: {err}"
    );
}

#[test]
fn test_legacy_policy_store_with_null_schema_succeeds() {
    let json = json!({
        "cedar_version": "v4.0.0",
        "policy_stores": {
            "test": {
                "name": "test",
                "schema": null,
                "policies": {}
            }
        }
    });

    let result = serde_json::from_str::<LegacyAgamaPolicyStore>(&json.to_string());
    let agama = result.expect("should deserialize with null schema");
    let (id, legacy_store) = agama.policy_stores.iter().next().expect("has one store");
    assert_eq!(id, "test", "store id should match");
    assert!(
        legacy_store.schema.is_none(),
        "schema should be None when null in JSON"
    );

    // Verify conversion to PolicyStore also yields schema: None
    let store: super::super::PolicyStore = legacy_store.clone().into();
    assert!(
        store.schema.is_none(),
        "converted PolicyStore should have None schema"
    );
}

#[test]
fn test_legacy_policy_store_missing_schema_field_succeeds() {
    let json = json!({
        "cedar_version": "v4.0.0",
        "policy_stores": {
            "test": {
                "name": "test",
                "policies": {}
            }
        }
    });

    let result = serde_json::from_str::<LegacyAgamaPolicyStore>(&json.to_string());
    let agama = result.expect("should deserialize with missing schema field");
    let (_, legacy_store) = agama.policy_stores.iter().next().expect("has one store");
    assert!(
        legacy_store.schema.is_none(),
        "schema should be None when field is absent"
    );

    let store: super::super::PolicyStore = legacy_store.clone().into();
    assert!(
        store.schema.is_none(),
        "converted PolicyStore should have None schema"
    );
}

#[test]
fn test_legacy_policy_store_invalid_schema_format_with_non_null_still_errors() {
    let json = json!({
        "cedar_version": "v4.0.0",
        "policy_stores": {
            "test": {
                "name": "test",
                "schema": "invalid_schema",
                "policies": {}
            }
        }
    });

    let result = serde_json::from_str::<LegacyAgamaPolicyStore>(&json.to_string());
    let err = result.expect_err("should error on non-null invalid schema");
    assert!(
        err.to_string().contains("error parsing schema"),
        "error should indicate schema parsing failure, got: {err}"
    );
}

#[test]
fn test_invalid_trusted_issuers_format() {
    let schema = base64::prelude::BASE64_STANDARD.encode("{}");
    let json = json!({
        "cedar_version": "v4.0.0",
        "policy_stores": {
            "test": {
                "name": "test",
                "schema": schema,
                "policies": {},
                "trusted_issuers": {
                    "invalid_issuer": {
                        "name": "test",
                        "description": "test",
                        "openid_configuration_endpoint": "invalid_url"
                    }
                }
            }
        }
    });

    let result = serde_json::from_str::<LegacyAgamaPolicyStore>(&json.to_string());
    let err = result.expect_err("Expected error for invalid openid_configuration_endpoint URL");
    assert!(
        err.to_string()
            .contains("the `\"openid_configuration_endpoint\"` or `\"configuration_endpoint\"` is not a valid url"),
        "Error should mention invalid URL, got: {err}"
    );
}