use std::{
collections::{HashMap, HashSet},
path::PathBuf,
sync::Arc,
};
use serde::{Deserialize, Serialize};
use crate::platform::PermissionStatus;
use crate::process_model::{ProcessClass, ProcessId};
type PromptHandler = Arc<dyn Fn(ProcessId, &Capability) -> PermissionResult + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PathScope {
AppData,
Downloads,
UserSelected,
Any,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Capability {
OpenExternalUrl,
FilesystemRead {
scope: PathScope,
},
FilesystemWrite {
scope: PathScope,
},
ShellExecute,
ClipboardRead,
ClipboardWrite,
Notification,
Network {
hosts: Vec<String>,
},
Microphone,
Camera,
ScreenCapture,
}
impl Capability {
pub fn is_high_risk(&self) -> bool {
matches!(
self,
Capability::ShellExecute
| Capability::ClipboardRead
| Capability::Network { .. }
| Capability::Camera
| Capability::ScreenCapture
| Capability::FilesystemRead {
scope: PathScope::Any
}
| Capability::FilesystemWrite {
scope: PathScope::Any
}
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PermissionResult {
Granted,
Denied,
Prompt,
}
#[derive(Clone, Default)]
pub struct PermissionBroker {
grants: HashMap<ProcessId, HashSet<Capability>>,
process_classes: HashMap<ProcessId, ProcessClass>,
class_defaults: HashMap<ProcessClass, HashSet<Capability>>,
prompt_handler: Option<PromptHandler>,
}
impl std::fmt::Debug for PermissionBroker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PermissionBroker")
.field("grant_count", &self.grants.len())
.field("registered_processes", &self.process_classes.len())
.field("class_default_count", &self.class_defaults.len())
.field("has_prompt_handler", &self.prompt_handler.is_some())
.finish()
}
}
impl PermissionBroker {
pub fn new() -> Self {
Self {
grants: HashMap::new(),
process_classes: HashMap::new(),
class_defaults: HashMap::new(),
prompt_handler: None,
}
}
pub fn register_process(&mut self, process: ProcessId, class: ProcessClass) {
self.process_classes.insert(process, class);
}
pub fn unregister_process(&mut self, process: ProcessId) {
self.process_classes.remove(&process);
self.grants.remove(&process);
}
pub fn set_default_capabilities<I>(&mut self, class: ProcessClass, capabilities: I)
where
I: IntoIterator<Item = Capability>,
{
self.class_defaults
.insert(class, capabilities.into_iter().collect());
}
pub fn apply_threat_model(&mut self, model: &ThreatModel) {
self.set_default_capabilities(
ProcessClass::Ui,
model.defaults_for(ProcessClass::Ui).iter().cloned(),
);
self.set_default_capabilities(
ProcessClass::Worker,
model.defaults_for(ProcessClass::Worker).iter().cloned(),
);
self.set_default_capabilities(
ProcessClass::Media,
model.defaults_for(ProcessClass::Media).iter().cloned(),
);
self.set_default_capabilities(
ProcessClass::Extension,
model.defaults_for(ProcessClass::Extension).iter().cloned(),
);
}
pub fn set_prompt_handler(
&mut self,
handler: impl Fn(ProcessId, &Capability) -> PermissionResult + Send + Sync + 'static,
) {
self.prompt_handler = Some(Arc::new(handler));
}
pub fn with_prompt_handler(
mut self,
handler: impl Fn(ProcessId, &Capability) -> PermissionResult + Send + Sync + 'static,
) -> Self {
self.set_prompt_handler(handler);
self
}
pub fn grant(&mut self, process: ProcessId, capability: Capability) {
self.grants.entry(process).or_default().insert(capability);
}
pub fn grant_all<I>(&mut self, process: ProcessId, capabilities: I)
where
I: IntoIterator<Item = Capability>,
{
self.grants.entry(process).or_default().extend(capabilities);
}
pub fn revoke(&mut self, process: ProcessId, capability: &Capability) {
if let Some(set) = self.grants.get_mut(&process) {
let _ = std::collections::HashSet::<Capability>::remove(set, capability);
}
}
pub fn revoke_all(&mut self, process: ProcessId) {
self.grants.remove(&process);
}
fn granted_by_default(&self, process: ProcessId, capability: &Capability) -> bool {
self.process_classes
.get(&process)
.and_then(|class| self.class_defaults.get(class))
.is_some_and(|set| set.contains(capability))
}
pub fn prompt(&self, process: ProcessId, capability: &Capability) -> PermissionResult {
if let Some(handler) = &self.prompt_handler {
handler(process, capability)
} else {
PermissionResult::Denied
}
}
pub fn check(&self, process: ProcessId, capability: &Capability) -> PermissionResult {
if let Some(set) = self.grants.get(&process) {
if std::collections::HashSet::<Capability>::contains(set, capability) {
return PermissionResult::Granted;
}
}
if self.granted_by_default(process, capability) {
return PermissionResult::Granted;
}
if let Some(prompt_handler) = &self.prompt_handler {
return prompt_handler(process, capability);
}
PermissionResult::Denied
}
pub fn check_any(&self, process: ProcessId, capabilities: &[Capability]) -> PermissionResult {
let mut prompted = false;
for cap in capabilities {
match self.check(process, cap) {
PermissionResult::Granted => return PermissionResult::Granted,
PermissionResult::Prompt => prompted = true,
PermissionResult::Denied => {}
}
}
if prompted {
PermissionResult::Prompt
} else {
PermissionResult::Denied
}
}
pub fn capabilities(&self, process: ProcessId) -> Vec<Capability> {
let mut capabilities = self
.process_classes
.get(&process)
.and_then(|class| self.class_defaults.get(class))
.cloned()
.unwrap_or_default();
if let Some(grants) = self.grants.get(&process) {
capabilities.extend(grants.iter().cloned());
}
capabilities.into_iter().collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ThreatCategory {
UntrustedContent,
MaliciousPlugin,
CompromisedChildProcess,
UnsafeExternalUrl,
LocalPrivilegeLeak,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Threat {
pub category: ThreatCategory,
pub description: String,
pub surfaces: Vec<String>,
pub mitigations: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ThreatModel {
pub threats: Vec<Threat>,
pub ui_defaults: Vec<Capability>,
pub worker_defaults: Vec<Capability>,
pub media_defaults: Vec<Capability>,
pub extension_defaults: Vec<Capability>,
}
impl ThreatModel {
pub fn new() -> Self {
Self {
threats: Vec::new(),
ui_defaults: vec![
Capability::OpenExternalUrl,
Capability::ClipboardRead,
Capability::ClipboardWrite,
Capability::Notification,
],
worker_defaults: vec![
Capability::FilesystemRead {
scope: PathScope::AppData,
},
Capability::FilesystemWrite {
scope: PathScope::AppData,
},
],
media_defaults: vec![
Capability::Microphone,
Capability::Camera,
Capability::ScreenCapture,
],
extension_defaults: vec![],
}
}
pub fn add_threat(mut self, threat: Threat) -> Self {
self.threats.push(threat);
self
}
pub fn defaults_for(&self, class: ProcessClass) -> &[Capability] {
match class {
ProcessClass::Ui => &self.ui_defaults,
ProcessClass::Worker => &self.worker_defaults,
ProcessClass::Media => &self.media_defaults,
ProcessClass::Extension => &self.extension_defaults,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PermissionKind {
Camera,
Microphone,
Location,
FileAccess,
Network,
Notifications,
Accessibility,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PermissionRequest {
pub kind: PermissionKind,
pub reason: String,
}
#[derive(Debug, Clone, Default)]
pub struct PermissionManager {
states: HashMap<PermissionKind, PermissionStatus>,
}
impl PermissionManager {
pub fn new() -> Self {
Self {
states: HashMap::new(),
}
}
pub fn status(&self, kind: PermissionKind) -> PermissionStatus {
self.states
.get(&kind)
.copied()
.unwrap_or(PermissionStatus::NotDetermined)
}
pub fn request(
&mut self,
request: &PermissionRequest,
decide: impl FnOnce(&PermissionRequest) -> PermissionStatus,
) -> PermissionStatus {
let current = self.status(request.kind);
if current != PermissionStatus::NotDetermined {
return current;
}
let result = decide(request);
self.states.insert(request.kind, result);
result
}
pub fn set_status(&mut self, kind: PermissionKind, status: PermissionStatus) {
self.states.insert(kind, status);
}
pub fn revoke(&mut self, kind: PermissionKind) {
self.states.insert(kind, PermissionStatus::Denied);
}
pub fn all_statuses(&self) -> &HashMap<PermissionKind, PermissionStatus> {
&self.states
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CredentialEntry {
pub service: String,
pub account: String,
pub secret: Vec<u8>,
}
#[derive(Debug, Clone, Default)]
pub struct KeychainStore {
entries: HashMap<(String, String), CredentialEntry>,
}
impl KeychainStore {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub fn store(&mut self, entry: CredentialEntry) {
let key = (entry.service.clone(), entry.account.clone());
self.entries.insert(key, entry);
}
pub fn retrieve(&self, service: &str, account: &str) -> Option<&CredentialEntry> {
self.entries.get(&(service.to_owned(), account.to_owned()))
}
pub fn delete(&mut self, service: &str, account: &str) -> bool {
self.entries
.remove(&(service.to_owned(), account.to_owned()))
.is_some()
}
pub fn list(&self) -> Vec<(&str, &str)> {
self.entries
.values()
.map(|entry| (entry.service.as_str(), entry.account.as_str()))
.collect()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccessToken {
pub path: PathBuf,
pub token: String,
pub created_at: u64,
pub expires_at: Option<u64>,
}
#[derive(Debug, Clone, Default)]
pub struct AccessTokenStore {
tokens: HashMap<String, AccessToken>,
next_id: u64,
}
impl AccessTokenStore {
pub fn new() -> Self {
Self {
tokens: HashMap::new(),
next_id: 0,
}
}
pub fn issue(&mut self, path: PathBuf, now: u64, ttl_seconds: Option<u64>) -> String {
self.next_id += 1;
let token_str = format!(
"kat_{:016x}_{}",
self.next_id,
seahash::hash(path.to_string_lossy().as_bytes())
);
let access_token = AccessToken {
path,
token: token_str.clone(),
created_at: now,
expires_at: ttl_seconds.map(|ttl| now + ttl),
};
self.tokens.insert(token_str.clone(), access_token);
token_str
}
pub fn validate(&self, token: &str, now: u64) -> Option<&PathBuf> {
let entry = self.tokens.get(token)?;
if let Some(expires) = entry.expires_at {
if now >= expires {
return None;
}
}
Some(&entry.path)
}
pub fn revoke(&mut self, token: &str) -> bool {
self.tokens.remove(token).is_some()
}
pub fn list(&self) -> Vec<&AccessToken> {
self.tokens.values().collect()
}
pub fn purge_expired(&mut self, now: u64) -> usize {
let before = self.tokens.len();
self.tokens
.retain(|_, token| token.expires_at.map_or(true, |expires| now < expires));
before - self.tokens.len()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginPermissionManifest {
pub plugin_id: String,
pub required: Vec<PermissionKind>,
pub optional: Vec<PermissionKind>,
}
impl PluginPermissionManifest {
pub fn new(plugin_id: impl Into<String>) -> Self {
Self {
plugin_id: plugin_id.into(),
required: Vec::new(),
optional: Vec::new(),
}
}
pub fn validate(&self, granted: &HashSet<PermissionKind>) -> Result<(), Vec<PermissionKind>> {
let missing: Vec<PermissionKind> = self
.required
.iter()
.filter(|perm| !granted.contains(perm))
.copied()
.collect();
if missing.is_empty() {
Ok(())
} else {
Err(missing)
}
}
pub fn check_permission(&self, kind: PermissionKind) -> bool {
self.required.contains(&kind) || self.optional.contains(&kind)
}
pub fn has_required(&self) -> bool {
!self.required.is_empty()
}
pub fn all_permissions(&self) -> Vec<PermissionKind> {
let mut set = HashSet::new();
for perm in &self.required {
set.insert(*perm);
}
for perm in &self.optional {
set.insert(*perm);
}
set.into_iter().collect()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProcessLimits {
pub max_memory_bytes: Option<u64>,
pub max_cpu_percent: Option<f64>,
pub max_open_files: Option<u32>,
pub network_allowed: bool,
}
impl Default for ProcessLimits {
fn default() -> Self {
Self {
max_memory_bytes: None,
max_cpu_percent: None,
max_open_files: None,
network_allowed: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProcessCapability {
pub pid: u32,
pub name: String,
pub limits: ProcessLimits,
pub violations: Vec<String>,
}
impl ProcessCapability {
pub fn new(pid: u32, name: impl Into<String>, limits: ProcessLimits) -> Self {
Self {
pid,
name: name.into(),
limits,
violations: Vec::new(),
}
}
pub fn record_violation(&mut self, description: impl Into<String>) {
self.violations.push(description.into());
}
pub fn violation_count(&self) -> usize {
self.violations.len()
}
pub fn check_memory(&mut self, used_bytes: u64) -> bool {
if let Some(max) = self.limits.max_memory_bytes {
if used_bytes > max {
self.record_violation(format!("memory limit exceeded: {used_bytes} > {max}"));
return false;
}
}
true
}
pub fn check_cpu(&mut self, cpu_percent: f64) -> bool {
if let Some(max) = self.limits.max_cpu_percent {
if cpu_percent > max {
self.record_violation(format!("CPU limit exceeded: {cpu_percent:.1}% > {max:.1}%"));
return false;
}
}
true
}
pub fn check_network(&mut self) -> bool {
if !self.limits.network_allowed {
self.record_violation("network access denied".to_owned());
return false;
}
true
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum NetworkPolicy {
AllowAll,
#[default]
DenyAll,
AllowList(Vec<String>),
DenyList(Vec<String>),
}
impl NetworkPolicy {
pub fn check(&self, host: &str) -> bool {
match self {
NetworkPolicy::AllowAll => true,
NetworkPolicy::DenyAll => false,
NetworkPolicy::AllowList(hosts) => hosts.iter().any(|h| h == host),
NetworkPolicy::DenyList(hosts) => !hosts.iter().any(|h| h == host),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IpcSchema {
pub version: u32,
pub min_compatible: u32,
pub message_types: Vec<String>,
}
impl IpcSchema {
pub fn new(version: u32, min_compatible: u32, message_types: Vec<String>) -> Self {
Self {
version,
min_compatible,
message_types,
}
}
pub fn is_compatible(&self, other: &IpcSchema) -> bool {
self.version >= other.min_compatible && other.version >= self.min_compatible
}
pub fn negotiate(&self, other: &IpcSchema) -> Option<u32> {
if self.is_compatible(other) {
Some(self.version.min(other.version))
} else {
None
}
}
pub fn common_message_types(&self, other: &IpcSchema) -> Vec<String> {
let other_set: HashSet<&String> = other.message_types.iter().collect();
self.message_types
.iter()
.filter(|mt| other_set.contains(mt))
.cloned()
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_capability_high_risk() {
assert!(Capability::ShellExecute.is_high_risk());
assert!(Capability::ClipboardRead.is_high_risk());
assert!(
Capability::Network {
hosts: vec!["example.com".to_string()]
}
.is_high_risk()
);
assert!(!Capability::Notification.is_high_risk());
assert!(!Capability::OpenExternalUrl.is_high_risk());
}
#[test]
fn test_permission_broker_grant_and_check() {
let mut broker = PermissionBroker::new();
let process = ProcessId(1);
assert_eq!(
broker.check(process, &Capability::OpenExternalUrl),
PermissionResult::Denied
);
broker.grant(process, Capability::OpenExternalUrl);
assert_eq!(
broker.check(process, &Capability::OpenExternalUrl),
PermissionResult::Granted
);
}
#[test]
fn test_permission_broker_class_defaults() {
let process = ProcessId(2);
let mut broker = PermissionBroker::new();
broker.register_process(process, ProcessClass::Worker);
broker.set_default_capabilities(
ProcessClass::Worker,
[Capability::FilesystemRead {
scope: PathScope::AppData,
}],
);
assert_eq!(
broker.check(
process,
&Capability::FilesystemRead {
scope: PathScope::AppData,
},
),
PermissionResult::Granted
);
}
#[test]
fn test_permission_broker_revoke() {
let mut broker = PermissionBroker::new();
let process = ProcessId(1);
broker.grant(process, Capability::ClipboardRead);
broker.revoke(process, &Capability::ClipboardRead);
assert_eq!(
broker.check(process, &Capability::ClipboardRead),
PermissionResult::Denied
);
}
#[test]
fn test_permission_broker_check_any() {
let mut broker = PermissionBroker::new();
let process = ProcessId(1);
broker.grant(process, Capability::Notification);
assert_eq!(
broker.check_any(
process,
&[Capability::ShellExecute, Capability::Notification]
),
PermissionResult::Granted
);
assert_eq!(
broker.check_any(process, &[Capability::ShellExecute, Capability::Camera]),
PermissionResult::Denied
);
}
#[test]
fn test_threat_model_defaults() {
let model = ThreatModel::new();
assert!(!model.ui_defaults.is_empty());
assert!(!model.worker_defaults.is_empty());
assert!(!model.media_defaults.is_empty());
assert!(model.extension_defaults.is_empty());
}
#[test]
fn test_threat_model_serialization() {
let model = ThreatModel::new().add_threat(Threat {
category: ThreatCategory::UntrustedContent,
description: "User-generated HTML may contain scripts".to_string(),
surfaces: vec!["webview".to_string()],
mitigations: vec!["sanitize input".to_string()],
});
let json = serde_json::to_string(&model).unwrap();
let decoded: ThreatModel = serde_json::from_str(&json).unwrap();
assert_eq!(model, decoded);
}
#[test]
fn test_permission_broker_apply_threat_model() {
let process = ProcessId(3);
let mut broker = PermissionBroker::new();
broker.register_process(process, ProcessClass::Ui);
broker.apply_threat_model(&ThreatModel::new());
assert_eq!(
broker.check(process, &Capability::Notification),
PermissionResult::Granted
);
}
#[test]
fn test_permission_broker_prompt_handler() {
let process = ProcessId(4);
let broker = PermissionBroker::new().with_prompt_handler(|_, capability| {
if capability.is_high_risk() {
PermissionResult::Prompt
} else {
PermissionResult::Denied
}
});
assert_eq!(
broker.check(process, &Capability::ShellExecute),
PermissionResult::Prompt
);
}
#[test]
fn test_permission_manager_default_not_determined() {
let mgr = PermissionManager::new();
assert_eq!(
mgr.status(PermissionKind::Camera),
PermissionStatus::NotDetermined
);
}
#[test]
fn test_permission_manager_request_grants() {
let mut mgr = PermissionManager::new();
let req = PermissionRequest {
kind: PermissionKind::Camera,
reason: "Video call".to_string(),
};
let status = mgr.request(&req, |_| PermissionStatus::Granted);
assert_eq!(status, PermissionStatus::Granted);
assert_eq!(
mgr.status(PermissionKind::Camera),
PermissionStatus::Granted
);
}
#[test]
fn test_permission_manager_request_denied() {
let mut mgr = PermissionManager::new();
let req = PermissionRequest {
kind: PermissionKind::Microphone,
reason: "Audio recording".to_string(),
};
let status = mgr.request(&req, |_| PermissionStatus::Denied);
assert_eq!(status, PermissionStatus::Denied);
}
#[test]
fn test_permission_manager_does_not_re_prompt() {
let mut mgr = PermissionManager::new();
mgr.set_status(PermissionKind::Location, PermissionStatus::Denied);
let req = PermissionRequest {
kind: PermissionKind::Location,
reason: "Map".to_string(),
};
let status = mgr.request(&req, |_| PermissionStatus::Granted);
assert_eq!(status, PermissionStatus::Denied);
}
#[test]
fn test_permission_manager_revoke() {
let mut mgr = PermissionManager::new();
mgr.set_status(PermissionKind::Network, PermissionStatus::Granted);
mgr.revoke(PermissionKind::Network);
assert_eq!(
mgr.status(PermissionKind::Network),
PermissionStatus::Denied
);
}
#[test]
fn test_permission_manager_all_statuses() {
let mut mgr = PermissionManager::new();
mgr.set_status(PermissionKind::Camera, PermissionStatus::Granted);
mgr.set_status(PermissionKind::Microphone, PermissionStatus::Denied);
assert_eq!(mgr.all_statuses().len(), 2);
}
#[test]
fn test_permission_manager_restricted_status() {
let mut mgr = PermissionManager::new();
mgr.set_status(PermissionKind::Camera, PermissionStatus::Restricted);
assert_eq!(
mgr.status(PermissionKind::Camera),
PermissionStatus::Restricted
);
}
#[test]
fn test_keychain_store_and_retrieve() {
let mut store = KeychainStore::new();
store.store(CredentialEntry {
service: "github".to_string(),
account: "user1".to_string(),
secret: b"token123".to_vec(),
});
let entry = store.retrieve("github", "user1").unwrap();
assert_eq!(entry.secret, b"token123");
}
#[test]
fn test_keychain_retrieve_missing() {
let store = KeychainStore::new();
assert!(store.retrieve("nope", "nada").is_none());
}
#[test]
fn test_keychain_delete() {
let mut store = KeychainStore::new();
store.store(CredentialEntry {
service: "svc".to_string(),
account: "acct".to_string(),
secret: vec![1, 2, 3],
});
assert!(store.delete("svc", "acct"));
assert!(!store.delete("svc", "acct"));
assert!(store.is_empty());
}
#[test]
fn test_keychain_list() {
let mut store = KeychainStore::new();
store.store(CredentialEntry {
service: "a".to_string(),
account: "b".to_string(),
secret: vec![],
});
store.store(CredentialEntry {
service: "c".to_string(),
account: "d".to_string(),
secret: vec![],
});
assert_eq!(store.len(), 2);
assert_eq!(store.list().len(), 2);
}
#[test]
fn test_keychain_overwrite() {
let mut store = KeychainStore::new();
store.store(CredentialEntry {
service: "svc".to_string(),
account: "acct".to_string(),
secret: b"old".to_vec(),
});
store.store(CredentialEntry {
service: "svc".to_string(),
account: "acct".to_string(),
secret: b"new".to_vec(),
});
assert_eq!(store.len(), 1);
assert_eq!(store.retrieve("svc", "acct").unwrap().secret, b"new");
}
#[test]
fn test_access_token_issue_and_validate() {
let mut store = AccessTokenStore::new();
let token = store.issue(PathBuf::from("/tmp/file.txt"), 1000, Some(3600));
assert!(store.validate(&token, 1000).is_some());
assert_eq!(
store.validate(&token, 1000).unwrap(),
&PathBuf::from("/tmp/file.txt")
);
}
#[test]
fn test_access_token_expired() {
let mut store = AccessTokenStore::new();
let token = store.issue(PathBuf::from("/f"), 1000, Some(60));
assert!(store.validate(&token, 1059).is_some());
assert!(store.validate(&token, 1060).is_none());
}
#[test]
fn test_access_token_no_expiry() {
let mut store = AccessTokenStore::new();
let token = store.issue(PathBuf::from("/f"), 0, None);
assert!(store.validate(&token, u64::MAX - 1).is_some());
}
#[test]
fn test_access_token_revoke() {
let mut store = AccessTokenStore::new();
let token = store.issue(PathBuf::from("/f"), 0, None);
assert!(store.revoke(&token));
assert!(store.validate(&token, 0).is_none());
assert!(!store.revoke(&token));
}
#[test]
fn test_access_token_list() {
let mut store = AccessTokenStore::new();
store.issue(PathBuf::from("/a"), 0, None);
store.issue(PathBuf::from("/b"), 0, None);
assert_eq!(store.list().len(), 2);
}
#[test]
fn test_access_token_purge_expired() {
let mut store = AccessTokenStore::new();
store.issue(PathBuf::from("/a"), 0, Some(10));
store.issue(PathBuf::from("/b"), 0, Some(20));
store.issue(PathBuf::from("/c"), 0, None);
let purged = store.purge_expired(15);
assert_eq!(purged, 1);
assert_eq!(store.list().len(), 2);
}
#[test]
fn test_plugin_manifest_validate_ok() {
let manifest = PluginPermissionManifest {
plugin_id: "my-plugin".to_string(),
required: vec![PermissionKind::Network, PermissionKind::FileAccess],
optional: vec![PermissionKind::Notifications],
};
let granted: HashSet<PermissionKind> =
[PermissionKind::Network, PermissionKind::FileAccess]
.into_iter()
.collect();
assert!(manifest.validate(&granted).is_ok());
}
#[test]
fn test_plugin_manifest_validate_missing() {
let manifest = PluginPermissionManifest {
plugin_id: "p".to_string(),
required: vec![PermissionKind::Camera, PermissionKind::Microphone],
optional: vec![],
};
let granted: HashSet<PermissionKind> = [PermissionKind::Camera].into_iter().collect();
let err = manifest.validate(&granted).unwrap_err();
assert_eq!(err, vec![PermissionKind::Microphone]);
}
#[test]
fn test_plugin_manifest_check_permission() {
let manifest = PluginPermissionManifest {
plugin_id: "p".to_string(),
required: vec![PermissionKind::Camera],
optional: vec![PermissionKind::Location],
};
assert!(manifest.check_permission(PermissionKind::Camera));
assert!(manifest.check_permission(PermissionKind::Location));
assert!(!manifest.check_permission(PermissionKind::Network));
}
#[test]
fn test_plugin_manifest_has_required() {
let empty = PluginPermissionManifest::new("e");
assert!(!empty.has_required());
let with_req = PluginPermissionManifest {
plugin_id: "p".to_string(),
required: vec![PermissionKind::Camera],
optional: vec![],
};
assert!(with_req.has_required());
}
#[test]
fn test_plugin_manifest_all_permissions() {
let manifest = PluginPermissionManifest {
plugin_id: "p".to_string(),
required: vec![PermissionKind::Camera],
optional: vec![PermissionKind::Camera, PermissionKind::Network],
};
let all = manifest.all_permissions();
assert_eq!(all.len(), 2);
}
#[test]
fn test_plugin_manifest_serialization() {
let manifest = PluginPermissionManifest {
plugin_id: "test".to_string(),
required: vec![PermissionKind::Camera],
optional: vec![PermissionKind::Network],
};
let json = serde_json::to_string(&manifest).unwrap();
let decoded: PluginPermissionManifest = serde_json::from_str(&json).unwrap();
assert_eq!(manifest, decoded);
}
#[test]
fn test_process_capability_memory_ok() {
let limits = ProcessLimits {
max_memory_bytes: Some(1024 * 1024),
..Default::default()
};
let mut cap = ProcessCapability::new(100, "worker", limits);
assert!(cap.check_memory(512 * 1024));
assert_eq!(cap.violation_count(), 0);
}
#[test]
fn test_process_capability_memory_exceeded() {
let limits = ProcessLimits {
max_memory_bytes: Some(1024),
..Default::default()
};
let mut cap = ProcessCapability::new(101, "worker", limits);
assert!(!cap.check_memory(2048));
assert_eq!(cap.violation_count(), 1);
}
#[test]
fn test_process_capability_cpu_exceeded() {
let limits = ProcessLimits {
max_cpu_percent: Some(50.0),
..Default::default()
};
let mut cap = ProcessCapability::new(102, "renderer", limits);
assert!(cap.check_cpu(49.9));
assert!(!cap.check_cpu(75.0));
assert_eq!(cap.violation_count(), 1);
}
#[test]
fn test_process_capability_network_denied() {
let limits = ProcessLimits {
network_allowed: false,
..Default::default()
};
let mut cap = ProcessCapability::new(103, "sandbox", limits);
assert!(!cap.check_network());
assert_eq!(cap.violation_count(), 1);
}
#[test]
fn test_process_capability_network_allowed() {
let limits = ProcessLimits::default();
let mut cap = ProcessCapability::new(104, "app", limits);
assert!(cap.check_network());
assert_eq!(cap.violation_count(), 0);
}
#[test]
fn test_process_limits_serialization() {
let limits = ProcessLimits {
max_memory_bytes: Some(1_000_000),
max_cpu_percent: Some(80.0),
max_open_files: Some(256),
network_allowed: false,
};
let json = serde_json::to_string(&limits).unwrap();
let decoded: ProcessLimits = serde_json::from_str(&json).unwrap();
assert_eq!(limits, decoded);
}
#[test]
fn test_network_policy_allow_all() {
let policy = NetworkPolicy::AllowAll;
assert!(policy.check("anything.com"));
}
#[test]
fn test_network_policy_deny_all() {
let policy = NetworkPolicy::DenyAll;
assert!(!policy.check("anything.com"));
}
#[test]
fn test_network_policy_allow_list() {
let policy = NetworkPolicy::AllowList(vec!["api.example.com".to_string()]);
assert!(policy.check("api.example.com"));
assert!(!policy.check("evil.com"));
}
#[test]
fn test_network_policy_deny_list() {
let policy = NetworkPolicy::DenyList(vec!["evil.com".to_string()]);
assert!(!policy.check("evil.com"));
assert!(policy.check("good.com"));
}
#[test]
fn test_network_policy_default_is_deny_all() {
let policy = NetworkPolicy::default();
assert!(!policy.check("anything.com"));
}
#[test]
fn test_network_policy_serialization() {
let policy = NetworkPolicy::AllowList(vec!["a.com".to_string(), "b.com".to_string()]);
let json = serde_json::to_string(&policy).unwrap();
let decoded: NetworkPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(policy, decoded);
}
#[test]
fn test_ipc_schema_compatible() {
let a = IpcSchema::new(3, 1, vec!["ping".to_string()]);
let b = IpcSchema::new(2, 1, vec!["pong".to_string()]);
assert!(a.is_compatible(&b));
assert!(b.is_compatible(&a));
}
#[test]
fn test_ipc_schema_incompatible() {
let a = IpcSchema::new(3, 3, vec![]);
let b = IpcSchema::new(1, 1, vec![]);
assert!(!a.is_compatible(&b));
}
#[test]
fn test_ipc_schema_negotiate() {
let a = IpcSchema::new(5, 2, vec![]);
let b = IpcSchema::new(3, 1, vec![]);
assert_eq!(a.negotiate(&b), Some(3));
}
#[test]
fn test_ipc_schema_negotiate_incompatible() {
let a = IpcSchema::new(5, 4, vec![]);
let b = IpcSchema::new(3, 1, vec![]);
assert_eq!(a.negotiate(&b), None);
}
#[test]
fn test_ipc_schema_common_message_types() {
let a = IpcSchema::new(1, 1, vec!["ping".to_string(), "data".to_string()]);
let b = IpcSchema::new(1, 1, vec!["data".to_string(), "pong".to_string()]);
let common = a.common_message_types(&b);
assert_eq!(common, vec!["data".to_string()]);
}
#[test]
fn test_ipc_schema_serialization() {
let schema = IpcSchema::new(2, 1, vec!["hello".to_string()]);
let json = serde_json::to_string(&schema).unwrap();
let decoded: IpcSchema = serde_json::from_str(&json).unwrap();
assert_eq!(schema, decoded);
}
#[test]
fn test_ipc_schema_self_compatible() {
let schema = IpcSchema::new(1, 1, vec!["msg".to_string()]);
assert!(schema.is_compatible(&schema));
assert_eq!(schema.negotiate(&schema), Some(1));
}
#[test]
fn test_permission_broker_unregister() {
let mut broker = PermissionBroker::new();
let pid = ProcessId(10);
broker.register_process(pid, ProcessClass::Extension);
broker.grant(pid, Capability::Notification);
broker.unregister_process(pid);
assert_eq!(
broker.check(pid, &Capability::Notification),
PermissionResult::Denied
);
}
#[test]
fn test_permission_broker_revoke_all() {
let mut broker = PermissionBroker::new();
let pid = ProcessId(11);
broker.grant(pid, Capability::Camera);
broker.grant(pid, Capability::Microphone);
broker.revoke_all(pid);
assert!(broker.capabilities(pid).is_empty());
}
#[test]
fn test_credential_entry_clone() {
let entry = CredentialEntry {
service: "s".to_string(),
account: "a".to_string(),
secret: vec![42],
};
let cloned = entry.clone();
assert_eq!(cloned.secret, vec![42]);
}
#[test]
fn test_access_token_unique_ids() {
let mut store = AccessTokenStore::new();
let t1 = store.issue(PathBuf::from("/a"), 0, None);
let t2 = store.issue(PathBuf::from("/a"), 0, None);
assert_ne!(t1, t2);
}
#[test]
fn test_process_capability_multiple_violations() {
let limits = ProcessLimits {
max_memory_bytes: Some(100),
max_cpu_percent: Some(10.0),
network_allowed: false,
..Default::default()
};
let mut cap = ProcessCapability::new(200, "test", limits);
cap.check_memory(200);
cap.check_cpu(50.0);
cap.check_network();
assert_eq!(cap.violation_count(), 3);
}
}