use std::collections::HashMap;
use chio_core_types::crypto::Keypair;
use chio_core_types::receipt::metadata::GuardEvidence;
use chio_http_core::{
CallerIdentity, HttpAuthority, HttpAuthorityError, HttpAuthorityInput, HttpAuthorityPolicy,
HttpMethod, HttpReceipt, PreparedHttpEvaluation, TransportDenyInput, Verdict,
};
use crate::error::ChioTowerError;
use crate::identity::IdentityExtractor;
pub struct EvaluationResult {
pub verdict: Verdict,
pub receipt: HttpReceipt,
pub evidence: Vec<GuardEvidence>,
}
pub struct EvaluationInput<'a> {
pub method: &'a str,
pub path: &'a str,
pub query: &'a HashMap<String, String>,
pub caller: CallerIdentity,
pub headers: &'a http::HeaderMap,
pub body_hash: Option<String>,
pub body_length: u64,
}
pub(crate) type PreparedEvaluation = PreparedHttpEvaluation;
pub type RouteResolver = fn(&str, &str) -> String;
fn default_route_resolver(_method: &str, path: &str) -> String {
path.to_string()
}
#[derive(Clone)]
struct HttpReceiptSink {
keypair: std::sync::Arc<Keypair>,
store: std::sync::Arc<dyn chio_kernel::ReceiptStore>,
}
pub struct ChioEvaluator {
authority: HttpAuthority,
identity_extractor: IdentityExtractor,
route_resolver: RouteResolver,
fail_open: bool,
receipt_sink: Option<HttpReceiptSink>,
allow_ephemeral: bool,
}
impl ChioEvaluator {
pub fn new(keypair: Keypair, policy_hash: String) -> Self {
Self {
authority: HttpAuthority::new(keypair, policy_hash),
identity_extractor: crate::identity::extract_identity,
route_resolver: default_route_resolver,
fail_open: false,
receipt_sink: None,
allow_ephemeral: false,
}
}
pub fn new_ephemeral(keypair: Keypair, policy_hash: String) -> Self {
Self {
authority: HttpAuthority::new_ephemeral(keypair, policy_hash),
identity_extractor: crate::identity::extract_identity,
route_resolver: default_route_resolver,
fail_open: false,
receipt_sink: None,
allow_ephemeral: true,
}
}
#[must_use]
pub fn builder(keypair: Keypair, policy_hash: String) -> ChioEvaluatorBuilder {
ChioEvaluatorBuilder {
keypair,
policy_hash,
receipt_store: None,
revocation_store: None,
durable_admission: None,
allow_ephemeral: false,
identity_extractor: crate::identity::extract_identity,
route_resolver: default_route_resolver,
fail_open: false,
}
}
#[must_use]
pub fn with_identity_extractor(mut self, extractor: IdentityExtractor) -> Self {
self.identity_extractor = extractor;
self
}
#[must_use]
pub fn with_route_resolver(mut self, resolver: RouteResolver) -> Self {
self.route_resolver = resolver;
self
}
#[must_use]
pub fn with_fail_open(mut self, fail_open: bool) -> Self {
self.fail_open = fail_open;
self
}
pub fn is_fail_open(&self) -> bool {
self.fail_open
}
pub fn identity_extractor(&self) -> IdentityExtractor {
self.identity_extractor
}
pub fn route_resolver(&self) -> RouteResolver {
self.route_resolver
}
pub fn evaluate(&self, input: EvaluationInput<'_>) -> Result<EvaluationResult, ChioTowerError> {
let prepared = self.prepare(input)?;
let receipt = self.sign_decision_receipt(&prepared)?;
Ok(EvaluationResult {
verdict: prepared.verdict,
receipt,
evidence: prepared.evidence,
})
}
pub(crate) fn prepare(
&self,
input: EvaluationInput<'_>,
) -> Result<PreparedEvaluation, ChioTowerError> {
let presented_capability = extract_presented_capability(input.headers, input.query);
self.prepare_with_presented_capability(input, presented_capability)
}
pub(crate) fn prepare_with_presented_capability(
&self,
input: EvaluationInput<'_>,
presented_capability: Option<&str>,
) -> Result<PreparedEvaluation, ChioTowerError> {
let EvaluationInput {
method,
path,
query,
caller,
headers: _headers,
body_hash,
body_length,
} = input;
let http_method = parse_method(method)?;
let route_pattern = (self.route_resolver)(method, path);
self.authority
.prepare(HttpAuthorityInput {
request_id: uuid::Uuid::now_v7().to_string(),
method: http_method,
route_pattern,
path,
query,
caller,
body_hash,
body_length,
session_id: None,
capability_id_hint: None,
presented_capability,
requested_tool_server: None,
requested_tool_name: None,
requested_arguments: None,
execution_nonce: None,
model_metadata: None,
unsupported_authorization_extension: None,
policy: policy_mode(http_method),
})
.map_err(Into::into)
}
pub(crate) fn sign_decision_receipt(
&self,
prepared: &PreparedEvaluation,
) -> Result<HttpReceipt, ChioTowerError> {
self.authority
.sign_decision_receipt(prepared)
.map_err(Into::into)
}
pub(crate) fn finalize_receipt(
&self,
prepared: &PreparedEvaluation,
response_status: u16,
) -> Result<HttpReceipt, ChioTowerError> {
self.authority
.finalize_receipt(prepared, response_status, None)
.map_err(Into::into)
}
pub(crate) fn sign_transport_deny_receipt(
&self,
input: TransportDenyInput<'_>,
) -> Result<HttpReceipt, ChioTowerError> {
self.authority
.sign_transport_deny_receipt(input)
.map_err(Into::into)
}
pub(crate) fn receipts_are_audited(&self) -> bool {
self.receipt_sink.is_some() || self.allow_ephemeral
}
pub(crate) fn has_durable_receipt_sink(&self) -> bool {
self.receipt_sink.is_some()
}
pub(crate) fn persist_http_receipt(&self, receipt: &HttpReceipt) -> Result<(), ChioTowerError> {
let Some(sink) = &self.receipt_sink else {
return Ok(());
};
let chio_receipt = match receipt.to_chio_receipt_with_keypair(&sink.keypair) {
Ok(chio_receipt) => chio_receipt,
Err(error) => {
tracing::warn!(
%error,
receipt_id = %receipt.id,
"skipping durable persistence of an HTTP receipt that cannot be converted into a verifiable core receipt"
);
return Ok(());
}
};
sink.store
.append_chio_receipt(&chio_receipt)
.map_err(|error| {
ChioTowerError::ReceiptPersist(format!(
"failed to append HTTP receipt to durable receipt store: {error}"
))
})?;
Ok(())
}
}
fn extract_presented_capability<'a>(
headers: &'a http::HeaderMap,
query: &'a HashMap<String, String>,
) -> Option<&'a str> {
headers
.get("x-chio-capability")
.or_else(|| headers.get("X-Chio-Capability"))
.and_then(|value| value.to_str().ok())
.or_else(|| query.get("chio_capability").map(String::as_str))
}
fn policy_mode(method: HttpMethod) -> HttpAuthorityPolicy {
if method.is_safe() {
HttpAuthorityPolicy::SessionAllow
} else {
HttpAuthorityPolicy::DenyByDefault
}
}
pub struct ChioEvaluatorBuilder {
keypair: Keypair,
policy_hash: String,
receipt_store: Option<std::sync::Arc<dyn chio_kernel::ReceiptStore>>,
revocation_store: Option<std::sync::Arc<dyn chio_kernel::RevocationStore>>,
durable_admission: Option<DurableAdmissionStores>,
allow_ephemeral: bool,
identity_extractor: IdentityExtractor,
route_resolver: RouteResolver,
fail_open: bool,
}
struct DurableAdmissionStores {
store: std::sync::Arc<dyn chio_kernel::QualifiedAdmissionProjectionStore>,
outcome_store: std::sync::Arc<dyn chio_kernel::tool_outcome::QualifiedToolOutcomeStore>,
fence: chio_kernel::admission_operation::StoreMutationFence,
}
impl ChioEvaluatorBuilder {
#[must_use]
pub fn receipt_store(mut self, store: std::sync::Arc<dyn chio_kernel::ReceiptStore>) -> Self {
self.receipt_store = Some(store);
self
}
#[must_use]
pub fn revocation_store(
mut self,
store: std::sync::Arc<dyn chio_kernel::RevocationStore>,
) -> Self {
self.revocation_store = Some(store);
self
}
#[must_use]
pub fn durable_admission_stores(
mut self,
store: std::sync::Arc<dyn chio_kernel::QualifiedAdmissionProjectionStore>,
outcome_store: std::sync::Arc<dyn chio_kernel::tool_outcome::QualifiedToolOutcomeStore>,
fence: chio_kernel::admission_operation::StoreMutationFence,
) -> Self {
self.durable_admission = Some(DurableAdmissionStores {
store,
outcome_store,
fence,
});
self
}
#[must_use]
pub fn allow_ephemeral(mut self, allow: bool) -> Self {
self.allow_ephemeral = allow;
self
}
#[must_use]
pub fn with_identity_extractor(mut self, extractor: IdentityExtractor) -> Self {
self.identity_extractor = extractor;
self
}
#[must_use]
pub fn with_route_resolver(mut self, resolver: RouteResolver) -> Self {
self.route_resolver = resolver;
self
}
#[must_use]
pub fn with_fail_open(mut self, fail_open: bool) -> Self {
self.fail_open = fail_open;
self
}
pub fn build(self) -> Result<ChioEvaluator, ChioTowerError> {
let mut builder = HttpAuthority::builder()
.allow_ephemeral_receipt_log(self.allow_ephemeral)
.allow_ephemeral_revocation_store(self.allow_ephemeral);
let receipt_sink = self.receipt_store.as_ref().map(|store| HttpReceiptSink {
keypair: std::sync::Arc::new(self.keypair.clone()),
store: std::sync::Arc::clone(store),
});
if let Some(store) = self.receipt_store {
builder = builder.receipt_store(store);
}
if let Some(store) = self.revocation_store {
builder = builder.revocation_store(store);
}
if let Some(durable) = self.durable_admission {
builder = builder.durable_admission_stores(
durable.store,
durable.outcome_store,
durable.fence,
);
}
let authority = builder
.build(self.keypair, self.policy_hash)
.map_err(ChioTowerError::from)?;
Ok(ChioEvaluator {
authority,
identity_extractor: self.identity_extractor,
route_resolver: self.route_resolver,
fail_open: self.fail_open,
receipt_sink,
allow_ephemeral: self.allow_ephemeral,
})
}
}
impl From<HttpAuthorityError> for ChioTowerError {
fn from(value: HttpAuthorityError) -> Self {
match value {
HttpAuthorityError::CallerIdentity(message) => {
Self::IdentityExtraction(format!("hash failed: {message}"))
}
HttpAuthorityError::ContentHash(message) => {
Self::Evaluation(format!("content hash failed: {message}"))
}
HttpAuthorityError::Kernel(message) => Self::Evaluation(message),
HttpAuthorityError::PendingApproval {
approval_id,
kernel_receipt_id,
} => Self::Evaluation(match approval_id {
Some(approval_id) => format!(
"request requires approval; approval_id={approval_id}; kernel_receipt_id={kernel_receipt_id}"
),
None => format!(
"request requires approval; kernel_receipt_id={kernel_receipt_id}"
),
}),
HttpAuthorityError::ReceiptSign(message) => {
Self::ReceiptSign(format!("signing failed: {message}"))
}
}
}
}
impl Clone for ChioEvaluator {
fn clone(&self) -> Self {
Self {
authority: self.authority.clone(),
identity_extractor: self.identity_extractor,
route_resolver: self.route_resolver,
fail_open: self.fail_open,
receipt_sink: self.receipt_sink.clone(),
allow_ephemeral: self.allow_ephemeral,
}
}
}
pub(crate) fn parse_method(method: &str) -> Result<HttpMethod, ChioTowerError> {
match method.to_uppercase().as_str() {
"GET" => Ok(HttpMethod::Get),
"POST" => Ok(HttpMethod::Post),
"PUT" => Ok(HttpMethod::Put),
"PATCH" => Ok(HttpMethod::Patch),
"DELETE" => Ok(HttpMethod::Delete),
"HEAD" => Ok(HttpMethod::Head),
"OPTIONS" => Ok(HttpMethod::Options),
other => Err(ChioTowerError::Evaluation(format!(
"unsupported HTTP method: {other}"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use chio_core_types::capability::{
scope::ChioScope,
token::{CapabilityToken, CapabilityTokenBody},
};
use chio_http_core::{
http_authority_tool_grant, http_status_scope, CHIO_HTTP_STATUS_SCOPE_DECISION,
CHIO_HTTP_STATUS_SCOPE_FINAL,
};
fn valid_capability_token_json(id: &str, issuer: &Keypair) -> String {
let now = chrono::Utc::now().timestamp() as u64;
let token = CapabilityToken::sign(
CapabilityTokenBody {
id: id.to_string(),
issuer: issuer.public_key(),
subject: issuer.public_key(),
scope: ChioScope {
grants: vec![http_authority_tool_grant()],
..ChioScope::default()
},
issued_at: now.saturating_sub(60),
expires_at: now + 3600,
delegation_chain: Vec::new(),
aggregate_invocation_budget: None,
},
issuer,
)
.unwrap_or_else(|e| panic!("token sign failed: {e}"));
serde_json::to_string(&token).unwrap_or_else(|e| panic!("token serialize failed: {e}"))
}
fn evaluate(
evaluator: &ChioEvaluator,
method: &str,
path: &str,
query: &HashMap<String, String>,
caller: CallerIdentity,
headers: &http::HeaderMap,
) -> Result<EvaluationResult, ChioTowerError> {
evaluator.evaluate(EvaluationInput {
method,
path,
query,
caller,
headers,
body_hash: None,
body_length: 0,
})
}
#[test]
fn evaluate_safe_method_allowed() {
let keypair = Keypair::generate();
let evaluator = ChioEvaluator::new_ephemeral(keypair, "test-policy".to_string());
let caller = CallerIdentity::anonymous();
let headers = http::HeaderMap::new();
let result = evaluate(
&evaluator,
"GET",
"/pets",
&HashMap::new(),
caller,
&headers,
)
.unwrap_or_else(|e| panic!("evaluation failed: {e}"));
assert!(result.verdict.is_allowed());
assert!(result
.receipt
.verify_signature()
.unwrap_or_else(|e| panic!("verify failed: {e}")));
assert_eq!(
http_status_scope(result.receipt.metadata.as_ref()),
Some(CHIO_HTTP_STATUS_SCOPE_DECISION)
);
}
#[test]
fn builder_with_durable_stores_allows_safe_method() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let receipt_store: std::sync::Arc<dyn chio_kernel::ReceiptStore> = std::sync::Arc::new(
chio_store_sqlite::SqliteReceiptStore::open(dir.path().join("receipts.db"))?,
);
let revocation_store: std::sync::Arc<dyn chio_kernel::RevocationStore> =
std::sync::Arc::new(chio_store_sqlite::SqliteRevocationStore::open(
dir.path().join("revocations.db"),
)?);
let authority_database = dir.path().join("authority.db");
let authority_locks = dir.path().join("authority-locks");
std::fs::create_dir(&authority_locks)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o700))?;
std::fs::set_permissions(&authority_locks, std::fs::Permissions::from_mode(0o700))?;
}
chio_store_sqlite::SqliteAuthorityStore::provision(&authority_database, &authority_locks)?;
let authority_store = chio_store_sqlite::SqliteAuthorityStore::open_serving(
&authority_database,
&authority_locks,
)?;
let admission_store = std::sync::Arc::new(authority_store.admission_operation_store());
let outcome_store = std::sync::Arc::new(authority_store.tool_outcome_store());
let evaluator = ChioEvaluator::builder(
Keypair::generate(),
chio_core_types::crypto::sha256_hex(b"test-policy"),
)
.receipt_store(receipt_store)
.revocation_store(revocation_store)
.durable_admission_stores(
admission_store,
outcome_store,
authority_store.mutation_fence(),
)
.build()?;
assert!(!evaluator.is_fail_open());
let caller = CallerIdentity::anonymous();
let headers = http::HeaderMap::new();
let result = evaluate(
&evaluator,
"GET",
"/pets",
&HashMap::new(),
caller,
&headers,
)?;
assert!(
result.verdict.is_allowed(),
"a durable-backed evaluator must allow a safe request"
);
Ok(())
}
#[derive(Default)]
struct CountingReceiptStore {
appended: std::sync::atomic::AtomicUsize,
}
impl CountingReceiptStore {
fn append_count(&self) -> usize {
self.appended.load(std::sync::atomic::Ordering::SeqCst)
}
}
impl chio_kernel::ReceiptStore for CountingReceiptStore {
fn append_chio_receipt(
&self,
_receipt: &chio_core_types::receipt::body::ChioReceipt,
) -> Result<(), chio_kernel::ReceiptStoreError> {
self.appended
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(())
}
fn append_child_receipt(
&self,
_receipt: &chio_core_types::receipt::lineage::ChildRequestReceipt,
) -> Result<(), chio_kernel::ReceiptStoreError> {
Ok(())
}
}
#[test]
fn persists_a_well_formed_http_receipt_to_a_verifying_store(
) -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let keypair = Keypair::generate();
let store: std::sync::Arc<dyn chio_kernel::ReceiptStore> = std::sync::Arc::new(
chio_store_sqlite::SqliteReceiptStore::open(dir.path().join("receipts.db"))?,
);
let evaluator = ChioEvaluator::builder(keypair, "durable-policy".to_string())
.receipt_store(store)
.allow_ephemeral(true)
.build()?;
let result = evaluate(
&evaluator,
"GET",
"/pets",
&HashMap::new(),
CallerIdentity::anonymous(),
&http::HeaderMap::new(),
)?;
evaluator.persist_http_receipt(&result.receipt)?;
Ok(())
}
#[test]
fn skips_an_http_receipt_that_cannot_convert_to_a_verifiable_record(
) -> Result<(), Box<dyn std::error::Error>> {
let store = std::sync::Arc::new(CountingReceiptStore::default());
let sink_store: std::sync::Arc<dyn chio_kernel::ReceiptStore> = store.clone();
let evaluator = ChioEvaluator::builder(Keypair::generate(), "durable-policy".to_string())
.receipt_store(sink_store)
.allow_ephemeral(true)
.build()?;
let foreign_evaluator =
ChioEvaluator::new_ephemeral(Keypair::generate(), "foreign-policy".to_string());
let foreign = evaluate(
&foreign_evaluator,
"GET",
"/pets",
&HashMap::new(),
CallerIdentity::anonymous(),
&http::HeaderMap::new(),
)?;
evaluator.persist_http_receipt(&foreign.receipt)?;
assert_eq!(
store.append_count(),
0,
"an HTTP receipt that cannot be converted into a verifiable core receipt must not be appended"
);
Ok(())
}
#[test]
fn evaluate_unsafe_method_denied_without_capability() {
let keypair = Keypair::generate();
let evaluator = ChioEvaluator::new_ephemeral(keypair, "test-policy".to_string());
let caller = CallerIdentity::anonymous();
let headers = http::HeaderMap::new();
let result = evaluate(
&evaluator,
"POST",
"/pets",
&HashMap::new(),
caller,
&headers,
)
.unwrap_or_else(|e| panic!("evaluation failed: {e}"));
assert!(result.verdict.is_denied());
assert_eq!(result.receipt.response_status, 403);
assert!(result
.receipt
.verify_signature()
.unwrap_or_else(|e| panic!("verify failed: {e}")));
assert_eq!(
http_status_scope(result.receipt.metadata.as_ref()),
Some(CHIO_HTTP_STATUS_SCOPE_DECISION)
);
}
#[test]
fn evaluate_unsafe_method_allowed_with_capability() {
let keypair = Keypair::generate();
let evaluator = ChioEvaluator::new_ephemeral(keypair.clone(), "test-policy".to_string());
let caller = CallerIdentity::anonymous();
let mut headers = http::HeaderMap::new();
headers.insert(
"x-chio-capability",
http::HeaderValue::from_str(&valid_capability_token_json("cap-123", &keypair))
.unwrap_or_else(|e| panic!("header build failed: {e}")),
);
let result = evaluate(
&evaluator,
"POST",
"/pets",
&HashMap::new(),
caller,
&headers,
)
.unwrap_or_else(|e| panic!("evaluation failed: {e}"));
assert!(result.verdict.is_allowed());
assert_eq!(result.receipt.capability_id.as_deref(), Some("cap-123"));
assert_eq!(
http_status_scope(result.receipt.metadata.as_ref()),
Some(CHIO_HTTP_STATUS_SCOPE_DECISION)
);
}
#[test]
fn evaluate_invalid_method() {
let keypair = Keypair::generate();
let evaluator = ChioEvaluator::new_ephemeral(keypair, "test-policy".to_string());
let caller = CallerIdentity::anonymous();
let headers = http::HeaderMap::new();
let err = evaluate(
&evaluator,
"FOOBAR",
"/pets",
&HashMap::new(),
caller,
&headers,
);
assert!(err.is_err());
}
#[test]
fn evaluate_all_safe_methods() {
let keypair = Keypair::generate();
let evaluator = ChioEvaluator::new_ephemeral(keypair, "test-policy".to_string());
let headers = http::HeaderMap::new();
for method in &["GET", "HEAD", "OPTIONS"] {
let caller = CallerIdentity::anonymous();
let result = evaluate(
&evaluator,
method,
"/test",
&HashMap::new(),
caller,
&headers,
)
.unwrap_or_else(|e| panic!("evaluation failed for {method}: {e}"));
assert!(result.verdict.is_allowed(), "{method} should be allowed");
}
}
#[test]
fn evaluate_all_unsafe_methods_denied() {
let keypair = Keypair::generate();
let evaluator = ChioEvaluator::new_ephemeral(keypair, "test-policy".to_string());
let headers = http::HeaderMap::new();
for method in &["POST", "PUT", "PATCH", "DELETE"] {
let caller = CallerIdentity::anonymous();
let result = evaluate(
&evaluator,
method,
"/test",
&HashMap::new(),
caller,
&headers,
)
.unwrap_or_else(|e| panic!("evaluation failed for {method}: {e}"));
assert!(
result.verdict.is_denied(),
"{method} should be denied without capability"
);
}
}
#[test]
fn fail_open_mode() {
let keypair = Keypair::generate();
let evaluator =
ChioEvaluator::new_ephemeral(keypair, "test-policy".to_string()).with_fail_open(true);
assert!(evaluator.is_fail_open());
}
#[test]
fn custom_route_resolver() {
fn resolver(_method: &str, path: &str) -> String {
path.trim_end_matches('/').to_string()
}
let keypair = Keypair::generate();
let evaluator = ChioEvaluator::new_ephemeral(keypair, "test-policy".to_string())
.with_route_resolver(resolver);
let caller = CallerIdentity::anonymous();
let headers = http::HeaderMap::new();
let result = evaluate(
&evaluator,
"GET",
"/pets/",
&HashMap::new(),
caller,
&headers,
)
.unwrap_or_else(|e| panic!("evaluation failed: {e}"));
assert!(result.verdict.is_allowed());
assert_eq!(result.receipt.route_pattern, "/pets");
}
#[test]
fn parse_method_case_insensitive() {
assert!(parse_method("get").is_ok());
assert!(parse_method("Get").is_ok());
assert!(parse_method("GET").is_ok());
}
#[test]
fn evaluator_clone() {
let keypair = Keypair::generate();
let evaluator = ChioEvaluator::new_ephemeral(keypair, "test-policy".to_string());
let cloned = evaluator.clone();
let caller = CallerIdentity::anonymous();
let headers = http::HeaderMap::new();
let r1 = evaluate(
&evaluator,
"GET",
"/test",
&HashMap::new(),
caller.clone(),
&headers,
)
.unwrap_or_else(|e| panic!("r1 failed: {e}"));
let r2 = evaluate(&cloned, "GET", "/test", &HashMap::new(), caller, &headers)
.unwrap_or_else(|e| panic!("r2 failed: {e}"));
assert!(r1.verdict.is_allowed());
assert!(r2.verdict.is_allowed());
assert_eq!(r1.receipt.kernel_key, r2.receipt.kernel_key);
}
#[test]
fn evaluate_invalid_capability_is_denied() {
let keypair = Keypair::generate();
let evaluator = ChioEvaluator::new_ephemeral(keypair, "test-policy".to_string());
let caller = CallerIdentity::anonymous();
let mut headers = http::HeaderMap::new();
headers.insert(
"x-chio-capability",
http::HeaderValue::from_static("not-json"),
);
let result = evaluate(
&evaluator,
"POST",
"/pets",
&HashMap::new(),
caller,
&headers,
)
.unwrap_or_else(|e| panic!("evaluation failed: {e}"));
assert!(result.verdict.is_denied());
assert!(result.receipt.capability_id.is_none());
assert_eq!(
http_status_scope(result.receipt.metadata.as_ref()),
Some(CHIO_HTTP_STATUS_SCOPE_DECISION)
);
}
#[test]
fn evaluate_query_parameters_affect_content_hash() {
let keypair = Keypair::generate();
let evaluator = ChioEvaluator::new_ephemeral(keypair, "test-policy".to_string());
let caller = CallerIdentity::anonymous();
let headers = http::HeaderMap::new();
let mut query_a = HashMap::new();
query_a.insert("q".to_string(), "cats".to_string());
let mut query_b = HashMap::new();
query_b.insert("q".to_string(), "dogs".to_string());
let result_a = evaluate(
&evaluator,
"GET",
"/search",
&query_a,
caller.clone(),
&headers,
)
.unwrap_or_else(|e| panic!("evaluation failed: {e}"));
let result_b = evaluate(&evaluator, "GET", "/search", &query_b, caller, &headers)
.unwrap_or_else(|e| panic!("evaluation failed: {e}"));
assert_ne!(result_a.receipt.content_hash, result_b.receipt.content_hash);
}
#[test]
fn finalize_receipt_marks_final_scope() {
let keypair = Keypair::generate();
let evaluator = ChioEvaluator::new_ephemeral(keypair, "test-policy".to_string());
let caller = CallerIdentity::anonymous();
let headers = http::HeaderMap::new();
let query = HashMap::new();
let prepared = evaluator
.prepare(EvaluationInput {
method: "GET",
path: "/pets",
query: &query,
caller,
headers: &headers,
body_hash: None,
body_length: 0,
})
.unwrap_or_else(|e| panic!("prepare failed: {e}"));
let receipt = evaluator
.finalize_receipt(&prepared, 201)
.unwrap_or_else(|e| panic!("finalize failed: {e}"));
assert_eq!(receipt.response_status, 201);
assert_eq!(
http_status_scope(receipt.metadata.as_ref()),
Some(CHIO_HTTP_STATUS_SCOPE_FINAL)
);
assert!(receipt
.verify_signature()
.unwrap_or_else(|e| panic!("verify failed: {e}")));
}
}