use super::{CacheError, CacheKey};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustPolicy {
pub require_encryption_for_shared: bool,
pub authorized_scopes: HashSet<String>,
pub is_shared_cache: bool,
pub allow_public_content: bool,
}
impl Default for TrustPolicy {
fn default() -> Self {
Self {
require_encryption_for_shared: true, authorized_scopes: HashSet::new(),
is_shared_cache: false,
allow_public_content: false, }
}
}
impl TrustPolicy {
#[must_use]
pub fn local() -> Self {
Self {
require_encryption_for_shared: false, authorized_scopes: HashSet::new(),
is_shared_cache: false,
allow_public_content: true,
}
}
#[must_use]
pub fn shared() -> Self {
Self {
require_encryption_for_shared: true, authorized_scopes: HashSet::new(),
is_shared_cache: true,
allow_public_content: false, }
}
#[must_use]
pub fn shared_with_public() -> Self {
Self {
require_encryption_for_shared: false, authorized_scopes: HashSet::new(),
is_shared_cache: true,
allow_public_content: true,
}
}
pub fn add_authorized_scope(&mut self, scope: String) {
self.authorized_scopes.insert(scope);
}
pub fn remove_authorized_scope(&mut self, scope: &str) {
self.authorized_scopes.remove(scope);
}
pub fn check_access(&self, key: &CacheKey) -> Result<(), CacheError> {
if let Some(scope) = &key.grant_scope {
if self.authorized_scopes.is_empty() {
if scope != "public" && scope != "public-read" {
return Err(CacheError::TrustViolation(format!(
"No authorized scopes configured, only public content allowed. Requested scope: {}",
scope
)));
}
} else if !self.authorized_scopes.contains(scope) {
return Err(CacheError::TrustViolation(format!(
"Unauthorized grant scope: {}",
scope
)));
}
}
Ok(())
}
pub fn check_storage(&self, key: &CacheKey, content_encrypted: bool) -> Result<(), CacheError> {
self.check_access(key)?;
if self.is_shared_cache && self.require_encryption_for_shared {
if key.grant_scope.is_none() {
return Err(CacheError::TrustViolation(
"Shared cache storage requires an explicit grant scope".to_string(),
));
}
if !self.is_explicitly_public_content(key) && !content_encrypted {
return Err(CacheError::TrustViolation(format!(
"Private content requires encryption for shared cache. Grant scope: {:?}",
key.grant_scope
)));
}
}
Ok(())
}
fn is_explicitly_public_content(&self, key: &CacheKey) -> bool {
if !self.allow_public_content {
return false;
}
match &key.grant_scope {
Some(scope) => {
scope == "public" || scope == "public-read"
}
None => {
false
}
}
}
pub fn validate(&self) -> Result<(), TrustPolicyError> {
if self.is_shared_cache && self.require_encryption_for_shared && self.allow_public_content {
return Err(TrustPolicyError::ConflictingPolicy(
"Shared cache cannot both require encryption and allow public content".to_string(),
));
}
Ok(())
}
#[must_use]
pub fn summary(&self) -> TrustPolicySummary {
TrustPolicySummary {
cache_type: if self.is_shared_cache {
"shared"
} else {
"local"
}
.to_string(),
encryption_required: self.require_encryption_for_shared,
public_content_allowed: self.allow_public_content,
authorized_scope_count: self.authorized_scopes.len(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustPolicySummary {
pub cache_type: String,
pub encryption_required: bool,
pub public_content_allowed: bool,
pub authorized_scope_count: usize,
}
#[derive(Debug, thiserror::Error)]
pub enum TrustPolicyError {
#[error("Conflicting policy configuration: {0}")]
ConflictingPolicy(String),
#[error("Invalid scope: {0}")]
InvalidScope(String),
}
#[derive(Debug)]
pub struct TrustBoundaryChecker {
policy: TrustPolicy,
access_log: Vec<TrustAccessEvent>,
max_log_entries: usize,
}
impl TrustBoundaryChecker {
const DEFAULT_MAX_LOG_ENTRIES: usize = 1000;
#[must_use]
pub fn new(policy: TrustPolicy) -> Self {
Self::with_max_log_entries(policy, Self::DEFAULT_MAX_LOG_ENTRIES)
}
#[must_use]
pub fn with_max_log_entries(policy: TrustPolicy, max_log_entries: usize) -> Self {
Self {
policy,
access_log: Vec::new(),
max_log_entries: max_log_entries.max(1), }
}
pub fn check_access(&mut self, key: &CacheKey, operation: &str) -> Result<(), CacheError> {
let result = self.policy.check_access(key);
let event = TrustAccessEvent {
key: key.clone(),
operation: operation.to_string(),
allowed: result.is_ok(),
timestamp: std::time::SystemTime::now(),
};
if self.access_log.len() >= self.max_log_entries {
self.access_log.remove(0);
}
self.access_log.push(event);
result
}
#[must_use]
pub const fn access_log(&self) -> &Vec<TrustAccessEvent> {
&self.access_log
}
pub fn clear_log(&mut self) {
self.access_log.clear();
}
#[must_use]
pub const fn max_log_entries(&self) -> usize {
self.max_log_entries
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustAccessEvent {
pub key: CacheKey,
pub operation: String,
pub allowed: bool,
pub timestamp: std::time::SystemTime,
}
#[cfg(all(test, feature = "legacy-internal-test-harnesses"))]
mod tests {
use super::*;
#[test]
fn trust_policy_local_cache_defaults() {
let policy = TrustPolicy::local();
assert!(!policy.require_encryption_for_shared);
assert!(!policy.is_shared_cache);
assert!(policy.allow_public_content);
}
#[test]
fn trust_policy_shared_cache_secure_by_default() {
let policy = TrustPolicy::shared();
assert!(policy.require_encryption_for_shared);
assert!(policy.is_shared_cache);
assert!(!policy.allow_public_content);
}
#[test]
fn trust_policy_scope_authorization() {
let mut policy = TrustPolicy::local();
policy.add_authorized_scope("test-scope".to_string());
let key_authorized = CacheKey::new(
"manifest".to_string(),
"content".to_string(),
Some("test-scope".to_string()),
);
let key_unauthorized = CacheKey::new(
"manifest".to_string(),
"content".to_string(),
Some("other-scope".to_string()),
);
assert!(policy.check_access(&key_authorized).is_ok());
assert!(policy.check_access(&key_unauthorized).is_err());
}
#[test]
fn trust_policy_validation_catches_conflicts() {
let conflicted_policy = TrustPolicy {
require_encryption_for_shared: true,
is_shared_cache: true,
allow_public_content: true, authorized_scopes: HashSet::new(),
};
assert!(conflicted_policy.validate().is_err());
}
#[test]
fn trust_boundary_checker_logs_access() {
let policy = TrustPolicy::local();
let mut checker = TrustBoundaryChecker::new(policy);
let key = CacheKey::new("manifest".to_string(), "content".to_string(), None);
let result = checker.check_access(&key, "get");
assert!(result.is_ok());
assert_eq!(checker.access_log().len(), 1);
assert!(checker.access_log()[0].allowed);
assert_eq!(
checker.max_log_entries(),
TrustBoundaryChecker::DEFAULT_MAX_LOG_ENTRIES
);
}
#[test]
fn trust_boundary_checker_bounded_logging() {
let policy = TrustPolicy::local();
let mut checker = TrustBoundaryChecker::with_max_log_entries(policy, 3);
let key1 = CacheKey::new("manifest1".to_string(), "content1".to_string(), None);
let key2 = CacheKey::new("manifest2".to_string(), "content2".to_string(), None);
let key3 = CacheKey::new("manifest3".to_string(), "content3".to_string(), None);
let key4 = CacheKey::new("manifest4".to_string(), "content4".to_string(), None);
checker.check_access(&key1, "get").unwrap();
checker.check_access(&key2, "put").unwrap();
checker.check_access(&key3, "delete").unwrap();
assert_eq!(checker.access_log().len(), 3);
assert_eq!(checker.max_log_entries(), 3);
assert_eq!(checker.access_log()[0].key.content_hash, "content1");
assert_eq!(checker.access_log()[1].key.content_hash, "content2");
assert_eq!(checker.access_log()[2].key.content_hash, "content3");
checker.check_access(&key4, "verify").unwrap();
assert_eq!(checker.access_log().len(), 3);
assert_eq!(checker.access_log()[0].key.content_hash, "content2");
assert_eq!(checker.access_log()[1].key.content_hash, "content3");
assert_eq!(checker.access_log()[2].key.content_hash, "content4");
}
#[test]
fn trust_boundary_checker_custom_max_entries() {
let policy = TrustPolicy::local();
let mut checker = TrustBoundaryChecker::with_max_log_entries(policy, 100);
assert_eq!(checker.max_log_entries(), 100);
let policy2 = TrustPolicy::local();
let checker2 = TrustBoundaryChecker::with_max_log_entries(policy2, 0);
assert_eq!(checker2.max_log_entries(), 1);
}
#[test]
fn trust_boundary_checker_clear_log_preserves_limit() {
let policy = TrustPolicy::local();
let mut checker = TrustBoundaryChecker::with_max_log_entries(policy, 5);
let key = CacheKey::new("manifest".to_string(), "content".to_string(), None);
for i in 0..3 {
checker
.check_access(&key, &format!("operation{}", i))
.unwrap();
}
assert_eq!(checker.access_log().len(), 3);
checker.clear_log();
assert_eq!(checker.access_log().len(), 0);
assert_eq!(checker.max_log_entries(), 5);
for i in 0..7 {
checker
.check_access(&key, &format!("operation{}", i))
.unwrap();
}
assert_eq!(checker.access_log().len(), 5); }
#[test]
fn trust_policy_summary() {
let policy = TrustPolicy::shared();
let summary = policy.summary();
assert_eq!(summary.cache_type, "shared");
assert!(summary.encryption_required);
assert!(!summary.public_content_allowed);
assert_eq!(summary.authorized_scope_count, 0);
}
#[test]
fn empty_authorized_scopes_security() {
let policy = TrustPolicy::default();
let public_key = CacheKey::new(
"manifest123".to_string(),
"content456".to_string(),
Some("public".to_string()),
);
assert!(policy.check_access(&public_key).is_ok());
let public_read_key = CacheKey::new(
"manifest123".to_string(),
"content456".to_string(),
Some("public-read".to_string()),
);
assert!(policy.check_access(&public_read_key).is_ok());
let private_key = CacheKey::new(
"manifest123".to_string(),
"content456".to_string(),
Some("private-scope".to_string()),
);
let result = policy.check_access(&private_key);
assert!(result.is_err());
assert!(matches!(result, Err(CacheError::TrustViolation(_))));
let no_scope_key = CacheKey::new("manifest123".to_string(), "content456".to_string(), None);
assert!(policy.check_access(&no_scope_key).is_ok());
let mut policy_with_scopes = TrustPolicy::default();
policy_with_scopes.add_authorized_scope("allowed-scope".to_string());
let allowed_key = CacheKey::new(
"manifest123".to_string(),
"content456".to_string(),
Some("allowed-scope".to_string()),
);
assert!(policy_with_scopes.check_access(&allowed_key).is_ok());
let unauthorized_key = CacheKey::new(
"manifest123".to_string(),
"content456".to_string(),
Some("unauthorized-scope".to_string()),
);
let result = policy_with_scopes.check_access(&unauthorized_key);
assert!(result.is_err());
assert!(matches!(result, Err(CacheError::TrustViolation(_))));
}
#[test]
fn shared_cache_encryption_validation() {
let policy = TrustPolicy {
is_shared_cache: true,
require_encryption_for_shared: true,
allow_public_content: true,
..TrustPolicy::default()
};
let public_key = CacheKey::new(
"manifest123".to_string(),
"content456".to_string(),
Some("public".to_string()),
);
assert!(policy.check_storage(&public_key, false).is_ok());
let private_key = CacheKey::new(
"manifest123".to_string(),
"content456".to_string(),
Some("private".to_string()),
);
let result = policy.check_storage(&private_key, false);
assert!(result.is_err());
assert!(matches!(result, Err(CacheError::TrustViolation(_))));
let no_scope_key = CacheKey::new("manifest123".to_string(), "content456".to_string(), None);
let result = policy.check_storage(&no_scope_key, false);
assert!(result.is_err());
assert!(matches!(result, Err(CacheError::TrustViolation(_))));
let result = policy.check_storage(&no_scope_key, true);
assert!(result.is_err());
assert!(matches!(result, Err(CacheError::TrustViolation(_))));
let mut scoped_policy = TrustPolicy {
is_shared_cache: true,
require_encryption_for_shared: true,
allow_public_content: false,
..TrustPolicy::default()
};
scoped_policy.add_authorized_scope("private-encrypted".to_string());
let encrypted_private_key = CacheKey::new(
"manifest123".to_string(),
"content456".to_string(),
Some("private-encrypted".to_string()),
);
assert!(
scoped_policy
.check_storage(&encrypted_private_key, true)
.is_ok()
);
assert!(
scoped_policy
.check_storage(&encrypted_private_key, false)
.is_err()
);
let local_policy = TrustPolicy::local();
assert!(local_policy.check_storage(&private_key, false).is_ok());
assert!(local_policy.check_storage(&no_scope_key, false).is_ok());
}
#[test]
fn is_explicitly_public_content_checks_cache_key() {
let mut policy = TrustPolicy::shared();
policy.allow_public_content = true;
let public_key = CacheKey::new(
"manifest123".to_string(),
"content456".to_string(),
Some("public".to_string()),
);
assert!(policy.is_explicitly_public_content(&public_key));
let public_read_key = CacheKey::new(
"manifest123".to_string(),
"content456".to_string(),
Some("public-read".to_string()),
);
assert!(policy.is_explicitly_public_content(&public_read_key));
let private_key = CacheKey::new(
"manifest123".to_string(),
"content456".to_string(),
Some("private".to_string()),
);
assert!(!policy.is_explicitly_public_content(&private_key));
let no_scope_key = CacheKey::new("manifest123".to_string(), "content456".to_string(), None);
assert!(!policy.is_explicitly_public_content(&no_scope_key));
policy.allow_public_content = false;
assert!(!policy.is_explicitly_public_content(&public_key));
}
}