#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
#![allow(deprecated)]
use crate::{
AuthorizationError, Authorizer, Context, Decision, Entities, EntityUid, PolicyId, PolicySet,
Request, Schema, ValidationMode, Validator,
};
use cedar_policy_core::jsonvalue::JsonValueWithNoDuplicateKeys;
use serde::{Deserialize, Serialize};
use std::{
collections::HashSet,
env,
path::{Path, PathBuf},
str::FromStr,
};
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct JsonTest {
pub policies: String,
pub entities: String,
pub schema: String,
pub should_validate: bool,
#[serde(alias = "queries")]
pub requests: Vec<JsonRequest>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct JsonRequest {
pub desc: String,
#[serde(default)]
pub principal: Option<JsonValueWithNoDuplicateKeys>,
#[serde(default)]
pub action: Option<JsonValueWithNoDuplicateKeys>,
#[serde(default)]
pub resource: Option<JsonValueWithNoDuplicateKeys>,
pub context: JsonValueWithNoDuplicateKeys,
#[serde(default = "constant_true")]
pub enable_request_validation: bool,
pub decision: Decision,
#[serde(alias = "reasons")]
pub reason: Vec<PolicyId>,
pub errors: Vec<PolicyId>,
}
fn constant_true() -> bool {
true
}
pub fn resolve_integration_test_path(path: impl AsRef<Path>) -> PathBuf {
if path.as_ref().is_relative() {
if let Ok(integration_tests_env_var) = env::var("CEDAR_INTEGRATION_TESTS_PATH") {
return PathBuf::from(integration_tests_env_var);
}
let manifest_dir = env::var("CARGO_MANIFEST_DIR")
.expect("`CARGO_MANIFEST_DIR` should be set by Cargo at build-time.");
let mut full_path = PathBuf::from(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,
) -> crate::frontend::is_authorized::InterfaceResponse;
fn validate(
&self,
schema: cedar_policy_validator::ValidatorSchema,
policies: &cedar_policy_core::ast::PolicySet,
) -> IntegrationTestValidationResult;
}
#[allow(clippy::panic)]
pub fn parse_policies_from_test(test: &JsonTest) -> PolicySet {
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));
PolicySet::from_str(&policies_text)
.unwrap_or_else(|e| panic!("error parsing policy in file {}: {e}", &test.policies))
}
pub fn parse_policies_from_test_internal(test: &JsonTest) -> cedar_policy_core::ast::PolicySet {
parse_policies_from_test(test).ast
}
#[allow(clippy::panic)]
pub fn parse_schema_from_test(test: &JsonTest) -> Schema {
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));
Schema::from_str(&schema_text)
.unwrap_or_else(|e| panic!("error parsing schema in {}: {e}", &test.schema))
}
pub fn parse_schema_from_test_internal(test: &JsonTest) -> cedar_policy_validator::ValidatorSchema {
parse_schema_from_test(test).0
}
#[allow(clippy::panic)]
pub fn parse_entities_from_test(test: &JsonTest, schema: &Schema) -> Entities {
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));
Entities::from_json_file(&entities_json, Some(schema))
.unwrap_or_else(|e| panic!("error parsing entities in {}: {e}", &test.entities))
}
pub fn parse_entities_from_test_internal(
test: &JsonTest,
schema: &Schema,
) -> cedar_policy_core::entities::Entities {
parse_entities_from_test(test, schema).0
}
#[allow(clippy::panic)]
pub fn parse_request_from_test(
json_request: &JsonRequest,
schema: &Schema,
test_name: &str,
) -> Request {
let principal = json_request.principal.clone().map(|json| {
EntityUid::from_json(json.into()).unwrap_or_else(|e| {
panic!(
"Failed to parse principal for request \"{}\" in {}: {e}",
json_request.desc, test_name
)
})
});
let action = json_request.action.clone().map(|json| {
EntityUid::from_json(json.into()).unwrap_or_else(|e| {
panic!(
"Failed to parse action for request \"{}\" in {}: {e}",
json_request.desc, test_name
)
})
});
let resource = json_request.resource.clone().map(|json| {
EntityUid::from_json(json.into()).unwrap_or_else(|e| {
panic!(
"Failed to parse resource for request \"{}\" in {}: {e}",
json_request.desc, test_name
)
})
});
let context_schema = action.as_ref().map(|a| (schema, a));
let context = Context::from_json_value(json_request.context.clone().into(), context_schema)
.unwrap_or_else(|e| {
panic!(
"error parsing context for request \"{}\" in {}: {e}",
json_request.desc, test_name
)
});
Request::new(
principal,
action,
resource,
context,
if json_request.enable_request_validation {
Some(schema)
} else {
None
},
)
.unwrap_or_else(|e| {
panic!(
"error validating request \"{}\" in {}: {e}",
json_request.desc, test_name
)
})
}
pub fn parse_request_from_test_internal(
request: &JsonRequest,
schema: &Schema,
test_name: &str,
) -> cedar_policy_core::ast::Request {
parse_request_from_test(request, schema, test_name).0
}
#[allow(clippy::too_many_lines)]
#[allow(clippy::panic)]
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 test_name: String = jsonfile.display().to_string();
let jsonstr = std::fs::read_to_string(jsonfile.as_path())
.unwrap_or_else(|e| panic!("error reading from file {test_name}: {e}"));
let test: JsonTest =
serde_json::from_str(&jsonstr).unwrap_or_else(|e| panic!("error parsing {test_name}: {e}"));
let policies = parse_policies_from_test(&test);
let schema = parse_schema_from_test(&test);
let entities = parse_entities_from_test(&test, &schema);
let validation_result = if let Some(custom_impl) = custom_impl_opt {
custom_impl.validate(schema.0.clone(), &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 {test_name}: {}",
validation_result.validation_errors_debug
);
} else {
assert!(
!validation_result.validation_passed,
"Expected that validation would fail in {test_name}, but it did not.",
);
}
for json_request in test.requests {
let request = parse_request_from_test(&json_request, &schema, &test_name);
if let Some(custom_impl) = custom_impl_opt {
let response = custom_impl.is_authorized(&request.0, &policies.ast, &entities.0);
assert_eq!(
response.decision(),
json_request.decision,
"test {test_name} failed for request \"{}\": unexpected decision",
&json_request.desc
);
let reasons: HashSet<PolicyId> = response.diagnostics().reason().cloned().collect();
assert_eq!(
reasons,
json_request.reason.into_iter().collect(),
"test {test_name} failed for request \"{}\": unexpected reasons",
&json_request.desc
);
} else {
let response = Authorizer::new().is_authorized(&request, &policies, &entities);
assert_eq!(
response.decision(),
json_request.decision,
"test {test_name} failed for request \"{}\": unexpected decision",
&json_request.desc
);
let reasons: HashSet<PolicyId> = response.diagnostics().reason().cloned().collect();
assert_eq!(
reasons,
json_request.reason.into_iter().collect(),
"test {test_name} failed for request \"{}\": unexpected reasons",
&json_request.desc
);
let errors: HashSet<PolicyId> = response
.diagnostics()
.errors()
.map(AuthorizationError::id)
.cloned()
.collect();
assert_eq!(
errors,
json_request.errors.into_iter().collect(),
"test {test_name} failed for request \"{}\": unexpected errors",
&json_request.desc
);
};
}
}
pub fn perform_integration_test_from_json(jsonfile: impl AsRef<Path>) {
perform_integration_test_from_json_custom(jsonfile, None);
}