#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
use crate::{
Authorizer, Context, Decision, Entities, EntityUid, EvaluationError, Policy, PolicyId,
PolicySet, Request, Response, Schema, ValidationMode, Validator,
};
use serde::Deserialize;
use std::{
env,
path::{Path, PathBuf},
str::FromStr,
};
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct JsonTest {
policies: String,
entities: String,
schema: String,
should_validate: bool,
#[serde(alias = "queries")]
requests: Vec<JsonRequest>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct JsonRequest {
desc: String,
#[serde(default)]
principal: Option<serde_json::Value>,
#[serde(default)]
action: Option<serde_json::Value>,
#[serde(default)]
resource: Option<serde_json::Value>,
context: serde_json::Value,
decision: Decision,
reasons: Vec<String>,
errors: Vec<String>,
}
pub fn resolve_integration_test_path(path: impl AsRef<Path>) -> PathBuf {
if path.as_ref().is_relative() {
let mut full_path = PathBuf::new();
let manifest_dir = env::var("CARGO_MANIFEST_DIR")
.expect("`CARGO_MANIFEST_DIR` should be set by Cargo at build-time.");
full_path.push(manifest_dir.clone());
full_path.push("..");
if manifest_dir.ends_with("cedar-drt") {
full_path.push("cedar");
}
full_path.push("cedar-integration-tests");
full_path.push(path.as_ref());
full_path
} else {
path.as_ref().into()
}
}
#[derive(Debug)]
pub struct IntegrationTestValidationResult {
pub validation_passed: bool,
pub validation_errors_debug: String,
}
pub trait CustomCedarImpl {
fn is_authorized(
&self,
q: &cedar_policy_core::ast::Request,
p: &cedar_policy_core::ast::PolicySet,
e: &cedar_policy_core::entities::Entities,
) -> Response;
fn validate(
&self,
schema: cedar_policy_validator::ValidatorSchema,
policies: &cedar_policy_core::ast::PolicySet,
) -> IntegrationTestValidationResult;
}
#[allow(clippy::too_many_lines)]
pub fn perform_integration_test_from_json_custom(
jsonfile: impl AsRef<Path>,
custom_impl_opt: Option<&dyn CustomCedarImpl>,
) {
let jsonfile = resolve_integration_test_path(jsonfile);
eprintln!("File path: {jsonfile:?}");
let jsonstr = std::fs::read_to_string(jsonfile.as_path())
.unwrap_or_else(|e| panic!("error reading from file {}: {e}", jsonfile.display()));
let test: JsonTest = serde_json::from_str(&jsonstr)
.unwrap_or_else(|e| panic!("error parsing {}: {e}", jsonfile.display()));
let policy_file = resolve_integration_test_path(&test.policies);
let policies_text = std::fs::read_to_string(policy_file)
.unwrap_or_else(|e| panic!("error loading policy file {}: {e}", &test.policies));
let policies_res = PolicySet::from_str(&policies_text);
if policies_res.is_err() {
for json_request in test.requests {
assert_eq!(
json_request.decision,
Decision::Deny,
"test {} failed for request \"{}\" \n Parse errors should only occur for deny",
jsonfile.display(),
&json_request.desc
);
}
return;
}
let policies = policies_res
.unwrap_or_else(|e| panic!("error parsing policy in file {}: {e}", &test.policies));
let schema_file = resolve_integration_test_path(&test.schema);
let schema_text = std::fs::read_to_string(schema_file)
.unwrap_or_else(|e| panic!("error loading schema file {}: {e}", &test.schema));
let schema = Schema::from_str(&schema_text)
.unwrap_or_else(|e| panic!("error parsing schema in {}: {e}", &test.schema));
let entity_file = resolve_integration_test_path(&test.entities);
let entities_json = std::fs::OpenOptions::new()
.read(true)
.open(entity_file)
.unwrap_or_else(|e| panic!("error opening entity file {}: {e}", &test.entities));
let entities = Entities::from_json_file(&entities_json, Some(&schema))
.unwrap_or_else(|e| panic!("error parsing entities in {}: {e}", &test.entities));
let validation_result = if let Some(custom_impl) = custom_impl_opt {
custom_impl.validate(schema.clone().0, &policies.ast)
} else {
let validator = Validator::new(schema.clone());
let api_result = validator.validate(&policies, ValidationMode::default());
IntegrationTestValidationResult {
validation_passed: api_result.validation_passed(),
validation_errors_debug: format!(
"{:?}",
api_result.validation_errors().collect::<Vec<_>>()
),
}
};
if test.should_validate {
assert!(
validation_result.validation_passed,
"Unexpected validation errors in {}: {}",
jsonfile.display(),
validation_result.validation_errors_debug
);
} else {
assert!(
!validation_result.validation_passed,
"Expected that validation would fail in {}, but it did not.",
jsonfile.display(),
);
}
for json_request in test.requests {
let principal = json_request.principal.map(|json| {
EntityUid::from_json(json).unwrap_or_else(|e| {
panic!(
"Failed to parse principal for request \"{}\" in {}: {e}",
json_request.desc,
jsonfile.display()
)
})
});
let action = json_request.action.map(|json| {
EntityUid::from_json(json).unwrap_or_else(|e| {
panic!(
"Failed to parse action for request \"{}\" in {}: {e}",
json_request.desc,
jsonfile.display()
)
})
});
let resource = json_request.resource.map(|json| {
EntityUid::from_json(json).unwrap_or_else(|e| {
panic!(
"Failed to parse resource for request \"{}\" in {}: {e}",
json_request.desc,
jsonfile.display()
)
})
});
let context_schema = action.as_ref().map(|a| (&schema, a));
let context = Context::from_json_value(json_request.context, context_schema)
.unwrap_or_else(|e| {
panic!(
"error parsing context for request \"{}\" in {}: {e}",
json_request.desc,
jsonfile.display()
)
});
let request = Request::new(principal, action, resource, context);
let response = if let Some(custom_impl) = custom_impl_opt {
custom_impl.is_authorized(&request.0, &policies.ast, &entities.0)
} else {
Authorizer::new().is_authorized(&request, &policies, &entities)
};
let expected_response = Response::new(
json_request.decision,
json_request
.reasons
.into_iter()
.map(|s| PolicyId::from_str(&s).unwrap())
.collect(),
json_request.errors.into_iter().collect(),
);
let mut parsing_fn_name: Option<String> = None;
for e in response.diagnostics().errors() {
let EvaluationError::StringMessage(msg) = e;
if msg.contains("poorly formed: invalid syntax, expected function, found") {
parsing_fn_name = Some(msg.split_whitespace().last().unwrap().to_string());
break;
}
}
if parsing_fn_name.is_some() {
assert_eq!(
response.decision(),
expected_response.decision(),
"test {} failed for request \"{}\"",
jsonfile.display(),
&json_request.desc
);
let mut found_matching_non_existent_fn_fuzzing = false;
for e in expected_response.diagnostics().errors() {
let EvaluationError::StringMessage(msg) = e;
if msg.contains(
"error occurred while evaluating policy `policy0`: function does not exist:",
) {
let fuzzing_fn_name = Some(msg.split_whitespace().last().unwrap().to_string());
if parsing_fn_name == fuzzing_fn_name {
found_matching_non_existent_fn_fuzzing = true;
break;
}
}
}
assert!(
found_matching_non_existent_fn_fuzzing,
"test {} failed for request \"{}\" \n Non existent function names did not match.",
jsonfile.display(),
&json_request.desc
);
} else {
assert_eq!(
response,
expected_response,
"test {} failed for request \"{}\"",
jsonfile.display(),
&json_request.desc
);
}
let ests = policies
.policies()
.map(|p| p.to_json().expect("should convert to JSON successfully"));
PolicySet::from_policies(ests.enumerate().map(|(i, est)| {
let id = PolicyId::from_str(&format!("policy{i}")).expect("id should be valid");
Policy::from_json(Some(id), est.clone()).unwrap_or_else(|e| {
panic!("in test {}, failed to build policy from JSON successfully: {e}\n\ntext policy was:\n{}\n\nJSON policy was: {}\n",
jsonfile.display(), policies.policies().nth(i).unwrap(), serde_json::to_string_pretty(&est).unwrap())
})
}))
.expect("should convert to PolicySet successfully");
}
}
pub fn perform_integration_test_from_json(jsonfile: impl AsRef<Path>) {
perform_integration_test_from_json_custom(jsonfile, None);
}