diff --git a/.justfile b/.justfile index 716dfd6..4dc9615 100644 --- a/.justfile +++ b/.justfile @@ -4,7 +4,7 @@ _help: # Run all unit tests using nextest. test: cargo nextest run --workspace --all-targets --future-incompat-report - cargo nextest run -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml --future-incompat-report + cargo nextest run -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml --run-ignored ignored-only --future-incompat-report # Run the fuzz tests against the wireline protocol. fuzz: @@ -27,8 +27,8 @@ fmt: ci: test fmt cargo clippy --workspace --all-targets cargo clippy -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml - cargo test --doc - cargo test --doc -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml + # cargo test --doc --no-tests + # cargo test --doc -p zerolease-store-postgres --manifest-path crates/zerolease-store-postgres/Cargo.toml --no-tests # Install required tools setup: @@ -40,11 +40,12 @@ setup: version BUMP: #!/usr/bin/env bash set -e - current=$(tomato get package.version Cargo.toml) + current=$(tomato get workspace.package.version Cargo.toml) version=$(semver-bump {{ BUMP }} "$current") - tomato set package.version "$version" Cargo.toml &> /dev/null + tomato set workspace.package.version "$version" Cargo.toml &> /dev/null + tomato set package.version "$version" crates/zerolease-store-postgres/Cargo.toml &> /dev/null cargo generate-lockfile - git commit Cargo.toml -m "v${version}" + git commit Cargo.{toml,lockfile} crates/zerolease-store-postgres/Cargo.{toml,lockfile} -m "v${version}" git tag "v${version}" echo "Release tagged for version v${version}" diff --git a/Cargo.toml b/Cargo.toml index 4769848..8789713 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ exclude = ["crates/zerolease-store-postgres"] resolver = "3" [workspace.package] +version = "0.1.0" edition = "2024" authors = ["C J Silverio "] license = "Apache-2.0" @@ -37,10 +38,10 @@ zerolease = { path = ".", default-features = false } [package] name = "zerolease" +description = "A lightweight, agent-aware credential vault with lease-based access control" authors.workspace = true -version = "0.1.0" +version.workspace = true edition.workspace = true -description = "A lightweight, agent-aware credential vault with lease-based access control" license.workspace = true repository.workspace = true keywords = ["credentials", "vault", "security", "agents", "secrets"] @@ -58,7 +59,7 @@ chrono.workspace = true secrecy.workspace = true serde.workspace = true serde_json.workspace = true -sha2 = "0.10" +sha2 = "0.11" thiserror.workspace = true tokio.workspace = true tracing = "0.1" diff --git a/crates/zerolease-agent/Cargo.toml b/crates/zerolease-agent/Cargo.toml index 035f18c..91956fb 100644 --- a/crates/zerolease-agent/Cargo.toml +++ b/crates/zerolease-agent/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zerolease-agent" -version = "0.1.0" description = "VM-side agent: credential provisioner, lease-aware proxy, git credential helper" +version.workspace = true edition.workspace = true authors.workspace = true license.workspace = true diff --git a/crates/zerolease-provider/Cargo.toml b/crates/zerolease-provider/Cargo.toml index b9c96fc..9907d93 100644 --- a/crates/zerolease-provider/Cargo.toml +++ b/crates/zerolease-provider/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zerolease-provider" -version = "0.1.0" description = "CredentialProvider trait for lease-based credential access in AI agent tools" +version.workspace = true edition.workspace = true authors.workspace = true license.workspace = true diff --git a/crates/zerolease-store-aws-sm/Cargo.toml b/crates/zerolease-store-aws-sm/Cargo.toml index c1723fd..bbe581c 100644 --- a/crates/zerolease-store-aws-sm/Cargo.toml +++ b/crates/zerolease-store-aws-sm/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zerolease-store-aws-sm" -version = "0.1.0" description = "AWS Secrets Manager-backed SecretStore for zerolease" +version.workspace = true edition.workspace = true authors.workspace = true license.workspace = true diff --git a/crates/zerolease-store-postgres/Cargo.toml b/crates/zerolease-store-postgres/Cargo.toml index bc0740c..7b50379 100644 --- a/crates/zerolease-store-postgres/Cargo.toml +++ b/crates/zerolease-store-postgres/Cargo.toml @@ -4,8 +4,8 @@ [package] name = "zerolease-store-postgres" -version = "0.1.0" description = "PostgreSQL-backed storage backends for zerolease (SecretStore + AuditLog)" +version = "0.1.0" edition = "2024" authors = ["C J Silverio "] license = "Apache-2.0" diff --git a/crates/zerolease-store-postgres/src/audit.rs b/crates/zerolease-store-postgres/src/audit.rs index 2248538..d4af76a 100644 --- a/crates/zerolease-store-postgres/src/audit.rs +++ b/crates/zerolease-store-postgres/src/audit.rs @@ -146,49 +146,91 @@ mod tests { } async fn test_audit_log() -> PostgresAuditLog { - let log = PostgresAuditLog::new(&test_url()) + PostgresAuditLog::new(&test_url()) .await - .expect("should create audit log"); - sqlx::query("DELETE FROM audit_events") - .execute(&log.pool) - .await - .expect("should clean test data"); - log + .expect("should create audit log") } fn make_entry(agent: &str, event: AuditEvent) -> AuditEntry { AuditEntry::new(event, AgentId::new(agent), &PeerIdentity::Anonymous, AuditOutcome::Success) } + /// Clean up audit events for a specific agent (each test cleans its own data). + async fn cleanup_agent(log: &PostgresAuditLog, agent: &str) { + sqlx::query("DELETE FROM audit_events WHERE agent = $1") + .bind(agent) + .execute(&log.pool) + .await + .expect("should clean up agent events"); + } + + /// Clean up audit events for a specific secret name. + async fn cleanup_secret(log: &PostgresAuditLog, secret: &str) { + sqlx::query("DELETE FROM audit_events WHERE secret_name = $1") + .bind(secret) + .execute(&log.pool) + .await + .expect("should clean up secret events"); + } + + /// Clean up audit events for a specific lease. + async fn cleanup_lease(log: &PostgresAuditLog, lease: &LeaseId) { + sqlx::query("DELETE FROM audit_events WHERE lease_id = $1") + .bind(lease.as_uuid().to_string()) + .execute(&log.pool) + .await + .expect("should clean up lease events"); + } + #[tokio::test] #[ignore] // requires running PostgreSQL with zerolease_test database async fn record_and_query_by_agent() { let log = test_audit_log().await; + // Use unique agent names to avoid collisions with other tests. + cleanup_agent(&log, "test-alice").await; + cleanup_agent(&log, "test-bob").await; + for _ in 0..3 { - log.record(make_entry("alice", AuditEvent::DekRotated)).await.expect("record"); + log.record(make_entry("test-alice", AuditEvent::DekRotated)) + .await + .expect("record should succeed"); } - log.record(make_entry("bob", AuditEvent::DekRotated)).await.expect("record"); + log.record(make_entry("test-bob", AuditEvent::DekRotated)) + .await + .expect("record should succeed"); + + let results = log.query_by_agent(&AgentId::new("test-alice"), 10).await.expect("query"); + assert_eq!(results.len(), 3, "alice should have 3 events"); - let results = log.query_by_agent(&AgentId::new("alice"), 10).await.expect("query"); - assert_eq!(results.len(), 3); + let bob = log.query_by_agent(&AgentId::new("test-bob"), 10).await.expect("query"); + assert_eq!(bob.len(), 1, "bob should have 1 event"); - let bob = log.query_by_agent(&AgentId::new("bob"), 10).await.expect("query"); - assert_eq!(bob.len(), 1); + cleanup_agent(&log, "test-alice").await; + cleanup_agent(&log, "test-bob").await; } #[tokio::test] #[ignore] async fn query_by_secret() { let log = test_audit_log().await; - log.record(make_entry("agent", AuditEvent::LeaseGranted { + cleanup_secret(&log, "pg-audit-secret-a").await; + + log.record(make_entry("audit-agent", AuditEvent::LeaseGranted { lease_id: LeaseId::new(), - secret_name: SecretName::new("pg-secret-a"), + secret_name: SecretName::new("pg-audit-secret-a"), domains: vec![DomainScope::new("api.example.com")], ttl_seconds: 900, - })).await.expect("record"); + })) + .await + .expect("record should succeed"); + + let results = log + .query_by_secret(&SecretName::new("pg-audit-secret-a"), 10) + .await + .expect("query should succeed"); + assert_eq!(results.len(), 1, "should find 1 event for the secret"); - let results = log.query_by_secret(&SecretName::new("pg-secret-a"), 10).await.expect("query"); - assert_eq!(results.len(), 1); + cleanup_secret(&log, "pg-audit-secret-a").await; } #[tokio::test] @@ -196,14 +238,19 @@ mod tests { async fn query_by_lease() { let log = test_audit_log().await; let lease = LeaseId::new(); - log.record(make_entry("agent", AuditEvent::LeaseGranted { + + log.record(make_entry("audit-agent", AuditEvent::LeaseGranted { lease_id: lease, - secret_name: SecretName::new("pg-secret"), + secret_name: SecretName::new("pg-audit-lease-secret"), domains: vec![DomainScope::new("example.com")], ttl_seconds: 900, - })).await.expect("record"); + })) + .await + .expect("record should succeed"); + + let results = log.query_by_lease(&lease).await.expect("query should succeed"); + assert_eq!(results.len(), 1, "should find 1 event for the lease"); - let results = log.query_by_lease(&lease).await.expect("query"); - assert_eq!(results.len(), 1); + cleanup_lease(&log, &lease).await; } } diff --git a/crates/zerolease-store-postgres/src/store.rs b/crates/zerolease-store-postgres/src/store.rs index 31b43e0..4633352 100644 --- a/crates/zerolease-store-postgres/src/store.rs +++ b/crates/zerolease-store-postgres/src/store.rs @@ -247,14 +247,9 @@ mod tests { } async fn test_store() -> PostgresStore { - let store = PostgresStore::new(&test_url()) + PostgresStore::new(&test_url()) .await - .expect("should create store"); - sqlx::query("DELETE FROM secrets") - .execute(&store.pool) - .await - .expect("should clean test data"); - store + .expect("should create store") } fn test_params(name: &str) -> StoreSecretParams { @@ -268,62 +263,101 @@ mod tests { } } + /// Clean up specific secrets by name (each test cleans up after itself). + async fn cleanup(store: &PostgresStore, names: &[&str]) { + for name in names { + let _ = store.delete(&SecretName::new(*name)).await; + } + } + #[tokio::test] #[ignore] // requires running PostgreSQL with zerolease_test database async fn put_and_get_round_trip() { let store = test_store().await; - let stored = store.put(test_params("pg-secret")).await.expect("put"); - assert_eq!(stored.name, SecretName::new("pg-secret")); - assert_eq!(stored.version, 1); - let fetched = store.get(&SecretName::new("pg-secret")).await.expect("get"); - assert_eq!(fetched.ciphertext, stored.ciphertext); + cleanup(&store, &["pg-roundtrip"]).await; + + let stored = store.put(test_params("pg-roundtrip")).await.expect("put should store secret"); + assert_eq!(stored.name, SecretName::new("pg-roundtrip"), "name should match"); + assert_eq!(stored.version, 1, "initial version should be 1"); + + let fetched = store.get(&SecretName::new("pg-roundtrip")).await.expect("get should retrieve secret"); + assert_eq!(fetched.ciphertext, stored.ciphertext, "ciphertext should match"); + + cleanup(&store, &["pg-roundtrip"]).await; } #[tokio::test] #[ignore] async fn put_duplicate_name_errors() { let store = test_store().await; - store.put(test_params("pg-dup")).await.expect("first put"); - let err = store.put(test_params("pg-dup")).await.expect_err("dup").to_string(); - assert!(err.contains("already exists"), "error was: {err}"); + cleanup(&store, &["pg-dup"]).await; + + store.put(test_params("pg-dup")).await.expect("first put should succeed"); + let err = store.put(test_params("pg-dup")).await.expect_err("duplicate put should fail").to_string(); + assert!(err.contains("already exists"), "expected 'already exists', got: {err}"); + + cleanup(&store, &["pg-dup"]).await; } #[tokio::test] #[ignore] async fn get_missing_errors() { let store = test_store().await; - let err = store.get(&SecretName::new("pg-nope")).await.expect_err("missing").to_string(); - assert!(err.contains("not found"), "error was: {err}"); + + let err = store + .get(&SecretName::new("pg-nonexistent")) + .await + .expect_err("get for missing secret should fail") + .to_string(); + assert!(err.contains("not found"), "expected 'not found', got: {err}"); } #[tokio::test] #[ignore] async fn update_increments_version() { let store = test_store().await; - store.put(test_params("pg-versioned")).await.expect("put"); + cleanup(&store, &["pg-versioned"]).await; + + store.put(test_params("pg-versioned")).await.expect("put should create secret"); let updated = store .update(&SecretName::new("pg-versioned"), vec![10, 20], vec![1; 12], CipherAlgorithm::Aes256Gcm) .await - .expect("update"); - assert_eq!(updated.version, 2); + .expect("update should succeed"); + assert_eq!(updated.version, 2, "version should increment to 2"); + + cleanup(&store, &["pg-versioned"]).await; } #[tokio::test] #[ignore] async fn delete_removes_secret() { let store = test_store().await; - store.put(test_params("pg-doomed")).await.expect("put"); - store.delete(&SecretName::new("pg-doomed")).await.expect("delete"); - assert!(store.get(&SecretName::new("pg-doomed")).await.is_err()); + cleanup(&store, &["pg-doomed"]).await; + + store.put(test_params("pg-doomed")).await.expect("put should create secret"); + store.delete(&SecretName::new("pg-doomed")).await.expect("delete should succeed"); + + let err = store + .get(&SecretName::new("pg-doomed")) + .await + .expect_err("get after delete should fail"); + assert!(err.to_string().contains("not found"), "expected 'not found' after delete"); } #[tokio::test] #[ignore] async fn list_returns_metadata() { let store = test_store().await; - store.put(test_params("pg-first")).await.expect("put"); - store.put(test_params("pg-second")).await.expect("put"); - let list = store.list().await.expect("list"); - assert_eq!(list.len(), 2); + cleanup(&store, &["pg-list-a", "pg-list-b"]).await; + + store.put(test_params("pg-list-a")).await.expect("put first"); + store.put(test_params("pg-list-b")).await.expect("put second"); + + let list = store.list().await.expect("list should succeed"); + let names: Vec = list.iter().map(|m| m.name.as_str().to_string()).collect(); + assert!(names.contains(&"pg-list-a".to_string()), "list should contain pg-list-a, got: {names:?}"); + assert!(names.contains(&"pg-list-b".to_string()), "list should contain pg-list-b, got: {names:?}"); + + cleanup(&store, &["pg-list-a", "pg-list-b"]).await; } } diff --git a/crates/zerolease-store-rusqlite/Cargo.toml b/crates/zerolease-store-rusqlite/Cargo.toml index 83ce0a4..1c21af5 100644 --- a/crates/zerolease-store-rusqlite/Cargo.toml +++ b/crates/zerolease-store-rusqlite/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zerolease-store-rusqlite" -version = "0.1.0" description = "Rusqlite-backed storage backends for zerolease (SecretStore + AuditLog)" +version.workspace = true edition.workspace = true authors.workspace = true license.workspace = true diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 73d9c5f..aa40d42 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zerolease-fuzz" -version = "0.0.0" +version = "0.1.0" publish = false edition = "2024"