mod common;
use std::collections::HashSet;
use chrono::{Duration, Utc};
use common::{generate_identity, IdentityMaterial};
use ed25519_dalek::SigningKey;
use gloves::{
agent::{backend::AgentBackend, meta::MetadataStore},
audit::AuditLog,
error::GlovesError,
human::{
backend::{HumanBackend, PassExecutor, PassOutput},
pending::PendingRequestStore,
},
manager::{SecretsManager, SetSecretOptions},
types::{AgentId, Owner, RequestStatus, SecretId, SecretMeta, SecretValue},
};
use rand::RngExt;
struct MockPass {
output: PassOutput,
}
impl PassExecutor for MockPass {
fn exec(&self, _args: &[&str]) -> gloves::error::Result<PassOutput> {
Ok(self.output.clone())
}
}
fn build_manager(human_backend: HumanBackend) -> (SecretsManager, tempfile::TempDir) {
let temp_dir = tempfile::tempdir().unwrap();
let manager = SecretsManager::new(
AgentBackend::new(temp_dir.path().join("store")).unwrap(),
human_backend,
MetadataStore::new(temp_dir.path().join("meta")).unwrap(),
PendingRequestStore::new(temp_dir.path().join("pending.json")).unwrap(),
AuditLog::new(temp_dir.path().join("audit.jsonl")).unwrap(),
);
(manager, temp_dir)
}
fn signing_key() -> SigningKey {
let mut key_bytes = [0_u8; 32];
rand::rng().fill(&mut key_bytes);
SigningKey::from_bytes(&key_bytes)
}
fn identity_for(temp_dir: &tempfile::TempDir, label: &str) -> IdentityMaterial {
generate_identity(temp_dir.path(), label)
}
#[test]
fn set_agent_secret() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&_temp, "agent-a");
let created = manager
.set(
id.clone(),
SecretValue::new(b"shh".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator,
recipients,
recipient_keys: vec![identity.recipient],
},
)
.unwrap();
assert_eq!(created, id);
}
#[test]
fn set_agent_secret_without_expiry() {
let (manager, temp_dir) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&temp_dir, "agent-a");
manager
.set(
id.clone(),
SecretValue::new(b"shh".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: None,
created_by: creator,
recipients,
recipient_keys: vec![identity.recipient],
},
)
.unwrap();
let metadata = manager.metadata_store.load(&id).unwrap();
assert!(metadata.expires_at.is_none());
}
#[test]
fn set_human_forbidden() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let recipients = HashSet::new();
let result = manager.set(
id,
SecretValue::new(b"shh".to_vec()),
SetSecretOptions {
owner: Owner::Human,
ttl: Some(Duration::hours(1)),
created_by: creator,
recipients,
recipient_keys: vec![],
},
);
assert!(matches!(result, Err(GlovesError::Forbidden)));
}
#[test]
fn set_rolls_back_ciphertext_when_metadata_save_fails() {
let (manager, temp_dir) = build_manager(HumanBackend::new());
std::fs::write(temp_dir.path().join("meta").join("service"), b"blocked").unwrap();
let secret_id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&temp_dir, "agent-a");
let result = manager.set(
secret_id.clone(),
SecretValue::new(b"value".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator,
recipients,
recipient_keys: vec![identity.recipient],
},
);
assert!(matches!(result, Err(GlovesError::Io(_))));
assert!(!manager.agent_backend.ciphertext_path(&secret_id).exists());
}
#[test]
fn set_rolls_back_metadata_and_ciphertext_when_audit_fails() {
let (manager, temp_dir) = build_manager(HumanBackend::new());
let audit_path = temp_dir.path().join("audit.jsonl");
std::fs::remove_file(&audit_path).unwrap();
std::fs::create_dir(&audit_path).unwrap();
let secret_id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&temp_dir, "agent-a");
let result = manager.set(
secret_id.clone(),
SecretValue::new(b"value".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator,
recipients,
recipient_keys: vec![identity.recipient],
},
);
assert!(matches!(result, Err(GlovesError::Io(_))));
assert!(!manager.agent_backend.ciphertext_path(&secret_id).exists());
assert!(manager.metadata_store.load(&secret_id).is_err());
}
#[test]
fn get_routes_agent() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&_temp, "agent-a");
manager
.set(
id.clone(),
SecretValue::new(b"agent-secret".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator.clone(),
recipients,
recipient_keys: vec![identity.recipient.clone()],
},
)
.unwrap();
let secret = manager
.get(&id, &creator, Some(identity.identity_file.as_path()))
.unwrap();
assert_eq!(secret.expose(|bytes| bytes.to_vec()), b"agent-secret");
}
#[test]
fn get_routes_human() {
let human_backend = HumanBackend::with_executor(Box::new(MockPass {
output: PassOutput {
status_code: 0,
stdout: "from-pass\n".to_owned(),
stderr: String::new(),
},
}));
let (manager, _temp) = build_manager(human_backend);
let id = SecretId::new("human/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
manager
.metadata_store
.save(&SecretMeta {
id: id.clone(),
owner: Owner::Human,
created_at: Utc::now(),
expires_at: Some(Utc::now() + Duration::hours(1)),
recipients: HashSet::new(),
created_by: creator.clone(),
last_accessed: None,
access_count: 0,
checksum: String::new(),
})
.unwrap();
let signing_key = signing_key();
let request = manager
.request(
id.clone(),
creator.clone(),
"approved access".to_owned(),
Duration::minutes(10),
&signing_key,
)
.unwrap();
manager
.approve_request(request.id, AgentId::new("reviewer-a").unwrap())
.unwrap();
let secret = manager.get(&id, &creator, None).unwrap();
assert_eq!(secret.expose(|bytes| bytes.to_vec()), b"from-pass");
}
#[test]
fn get_human_without_approval_forbidden() {
let human_backend = HumanBackend::with_executor(Box::new(MockPass {
output: PassOutput {
status_code: 0,
stdout: "from-pass\n".to_owned(),
stderr: String::new(),
},
}));
let (manager, _temp) = build_manager(human_backend);
let id = SecretId::new("human/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
manager
.metadata_store
.save(&SecretMeta {
id: id.clone(),
owner: Owner::Human,
created_at: Utc::now(),
expires_at: Some(Utc::now() + Duration::hours(1)),
recipients: HashSet::new(),
created_by: creator.clone(),
last_accessed: None,
access_count: 0,
checksum: String::new(),
})
.unwrap();
assert!(matches!(
manager.get(&id, &creator, None),
Err(GlovesError::Forbidden)
));
}
#[test]
fn get_expired() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&_temp, "agent-a");
manager
.set(
id.clone(),
SecretValue::new(b"agent-secret".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::seconds(-1)),
created_by: creator.clone(),
recipients,
recipient_keys: vec![identity.recipient.clone()],
},
)
.unwrap();
assert!(matches!(
manager.get(&id, &creator, Some(identity.identity_file.as_path())),
Err(GlovesError::Expired)
));
}
#[test]
fn get_unauthorized() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&_temp, "agent-a");
manager
.set(
id.clone(),
SecretValue::new(b"agent-secret".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator,
recipients,
recipient_keys: vec![identity.recipient],
},
)
.unwrap();
let outsider = AgentId::new("agent-b").unwrap();
let outsider_identity = identity_for(&_temp, "agent-b");
assert!(matches!(
manager.get(
&id,
&outsider,
Some(outsider_identity.identity_file.as_path())
),
Err(GlovesError::Unauthorized)
));
}
#[test]
fn get_agent_without_identity_is_unauthorized() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&_temp, "agent-a");
manager
.set(
id.clone(),
SecretValue::new(b"agent-secret".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator.clone(),
recipients,
recipient_keys: vec![identity.recipient],
},
)
.unwrap();
assert!(matches!(
manager.get(&id, &creator, None),
Err(GlovesError::Unauthorized)
));
}
#[test]
fn get_increments_access() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&_temp, "agent-a");
manager
.set(
id.clone(),
SecretValue::new(b"agent-secret".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator.clone(),
recipients,
recipient_keys: vec![identity.recipient.clone()],
},
)
.unwrap();
manager
.get(&id, &creator, Some(identity.identity_file.as_path()))
.unwrap();
let meta = manager.metadata_store.load(&id).unwrap();
assert_eq!(meta.access_count, 1);
}
#[test]
fn get_tampered_ciphertext_fails_integrity() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&_temp, "agent-a");
manager
.set(
id.clone(),
SecretValue::new(b"agent-secret".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator.clone(),
recipients,
recipient_keys: vec![identity.recipient.clone()],
},
)
.unwrap();
let ciphertext_path = manager.agent_backend.ciphertext_path(&id);
let mut bytes = std::fs::read(&ciphertext_path).unwrap();
bytes[0] ^= 0xFF;
std::fs::write(ciphertext_path, bytes).unwrap();
assert!(matches!(
manager.get(&id, &creator, Some(identity.identity_file.as_path())),
Err(GlovesError::IntegrityViolation)
));
}
#[test]
fn get_with_empty_checksum_allows_legacy_metadata() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&_temp, "agent-a");
manager
.set(
id.clone(),
SecretValue::new(b"agent-secret".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator.clone(),
recipients,
recipient_keys: vec![identity.recipient.clone()],
},
)
.unwrap();
let mut meta = manager.metadata_store.load(&id).unwrap();
meta.checksum.clear();
manager.metadata_store.save(&meta).unwrap();
let value = manager
.get(&id, &creator, Some(identity.identity_file.as_path()))
.unwrap();
assert_eq!(value.expose(|bytes| bytes.to_vec()), b"agent-secret");
}
#[test]
fn request_creates_pending() {
let (manager, _temp) = build_manager(HumanBackend::new());
let signing_key = signing_key();
manager
.request(
SecretId::new("human/token").unwrap(),
AgentId::new("agent-a").unwrap(),
"need deploy".to_owned(),
Duration::minutes(10),
&signing_key,
)
.unwrap();
assert_eq!(manager.pending_store.load_all().unwrap().len(), 1);
}
#[test]
fn grant_agent_ok() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let new_agent = AgentId::new("agent-b").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let creator_identity = identity_for(&_temp, "agent-a");
let new_identity = identity_for(&_temp, "agent-b");
let extra_identity = identity_for(&_temp, "agent-c");
manager
.set(
id.clone(),
SecretValue::new(b"agent-secret".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator.clone(),
recipients,
recipient_keys: vec![creator_identity.recipient.clone()],
},
)
.unwrap();
manager
.grant(
&id,
&creator,
creator_identity.identity_file.as_path(),
new_agent.clone(),
&[
new_identity.recipient.clone(),
extra_identity.recipient.clone(),
],
)
.unwrap();
let value = manager
.get(&id, &new_agent, Some(new_identity.identity_file.as_path()))
.unwrap();
assert_eq!(value.expose(|bytes| bytes.to_vec()), b"agent-secret");
}
#[test]
fn grant_human_forbidden() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("human/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
manager
.metadata_store
.save(&SecretMeta {
id: id.clone(),
owner: Owner::Human,
created_at: Utc::now(),
expires_at: Some(Utc::now() + Duration::hours(1)),
recipients: HashSet::new(),
created_by: creator.clone(),
last_accessed: None,
access_count: 0,
checksum: String::new(),
})
.unwrap();
let grant_identity = identity_for(&_temp, "grant-a");
let grant_recipient = identity_for(&_temp, "grant-b");
let result = manager.grant(
&id,
&creator,
grant_identity.identity_file.as_path(),
AgentId::new("agent-b").unwrap(),
&[grant_recipient.recipient],
);
assert!(matches!(result, Err(GlovesError::Forbidden)));
}
#[test]
fn grant_by_non_creator_forbidden() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let outsider = AgentId::new("agent-b").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let creator_identity = identity_for(&_temp, "agent-a");
manager
.set(
id.clone(),
SecretValue::new(b"agent-secret".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator,
recipients,
recipient_keys: vec![creator_identity.recipient],
},
)
.unwrap();
let outsider_identity = identity_for(&_temp, "agent-b");
let new_agent_identity = identity_for(&_temp, "agent-c");
let result = manager.grant(
&id,
&outsider,
outsider_identity.identity_file.as_path(),
AgentId::new("agent-c").unwrap(),
&[new_agent_identity.recipient],
);
assert!(matches!(result, Err(GlovesError::Forbidden)));
}
#[test]
fn revoke_by_creator() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&_temp, "agent-a");
manager
.set(
id.clone(),
SecretValue::new(b"agent-secret".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator.clone(),
recipients,
recipient_keys: vec![identity.recipient],
},
)
.unwrap();
manager.revoke(&id, &creator).unwrap();
assert!(manager.metadata_store.load(&id).is_err());
assert!(!manager.agent_backend.ciphertext_path(&id).exists());
}
#[test]
fn revoke_by_noncreator() {
let (manager, _temp) = build_manager(HumanBackend::new());
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&_temp, "agent-a");
manager
.set(
id.clone(),
SecretValue::new(b"agent-secret".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator,
recipients,
recipient_keys: vec![identity.recipient],
},
)
.unwrap();
let outsider = AgentId::new("agent-b").unwrap();
assert!(matches!(
manager.revoke(&id, &outsider),
Err(GlovesError::Forbidden)
));
}
#[test]
fn list_all() {
let (manager, _temp) = build_manager(HumanBackend::new());
let signing_key = signing_key();
let id = SecretId::new("service/token").unwrap();
let creator = AgentId::new("agent-a").unwrap();
let mut recipients = HashSet::new();
recipients.insert(creator.clone());
let identity = identity_for(&_temp, "agent-a");
manager
.set(
id,
SecretValue::new(b"agent-secret".to_vec()),
SetSecretOptions {
owner: Owner::Agent,
ttl: Some(Duration::hours(1)),
created_by: creator.clone(),
recipients,
recipient_keys: vec![identity.recipient],
},
)
.unwrap();
manager
.request(
SecretId::new("human/token").unwrap(),
creator,
"need deploy".to_owned(),
Duration::minutes(10),
&signing_key,
)
.unwrap();
let list = manager.list_all().unwrap();
assert_eq!(list.len(), 2);
assert!(serde_json::to_string(&list).unwrap().contains("pending"));
assert!(serde_json::to_string(&list).unwrap().contains("secret"));
}
#[test]
fn approve_and_deny_request_wrappers() {
let (manager, _temp) = build_manager(HumanBackend::new());
let signing_key = signing_key();
let approved_request = manager
.request(
SecretId::new("human/token").unwrap(),
AgentId::new("agent-a").unwrap(),
"need deploy".to_owned(),
Duration::minutes(10),
&signing_key,
)
.unwrap();
let approved = manager
.approve_request(approved_request.id, AgentId::new("reviewer-a").unwrap())
.unwrap();
assert_eq!(approved.status, RequestStatus::Fulfilled);
assert!(!approved.pending);
assert_eq!(
approved.approved_by,
Some(AgentId::new("reviewer-a").unwrap())
);
assert!(approved.approved_at.is_some());
assert!(matches!(
manager.deny_request(approved_request.id, AgentId::new("reviewer-b").unwrap()),
Err(GlovesError::InvalidInput(_))
));
let denied_request = manager
.request(
SecretId::new("human/token").unwrap(),
AgentId::new("agent-a").unwrap(),
"need deploy".to_owned(),
Duration::minutes(10),
&signing_key,
)
.unwrap();
let denied = manager
.deny_request(denied_request.id, AgentId::new("reviewer-b").unwrap())
.unwrap();
assert_eq!(denied.status, RequestStatus::Denied);
assert!(!denied.pending);
assert_eq!(denied.denied_by, Some(AgentId::new("reviewer-b").unwrap()));
assert!(denied.denied_at.is_some());
let approved_status = manager
.pending_store
.load_all()
.unwrap()
.into_iter()
.find(|entry| entry.id == approved_request.id)
.unwrap()
.status;
assert_eq!(approved_status, RequestStatus::Fulfilled);
let denied_status = manager
.pending_store
.load_all()
.unwrap()
.into_iter()
.find(|entry| entry.id == denied_request.id)
.unwrap()
.status;
assert_eq!(denied_status, RequestStatus::Denied);
}
#[test]
fn status_value_is_valid() {
assert_eq!(
serde_json::to_string(&RequestStatus::Pending).unwrap(),
"\"pending\""
);
}