Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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}"

Expand Down
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ exclude = ["crates/zerolease-store-postgres"]
resolver = "3"

[workspace.package]
version = "0.1.0"
edition = "2024"
authors = ["C J Silverio <ceejceej@gmail.com>"]
license = "Apache-2.0"
Expand Down Expand Up @@ -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"]
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion crates/zerolease-agent/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/zerolease-provider/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/zerolease-store-aws-sm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/zerolease-store-postgres/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ceejceej@gmail.com>"]
license = "Apache-2.0"
Expand Down
93 changes: 70 additions & 23 deletions crates/zerolease-store-postgres/src/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,64 +146,111 @@ 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]
#[ignore]
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;
}
}
88 changes: 61 additions & 27 deletions crates/zerolease-store-postgres/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<String> = 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;
}
}
2 changes: 1 addition & 1 deletion crates/zerolease-store-rusqlite/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "zerolease-fuzz"
version = "0.0.0"
version = "0.1.0"
publish = false
edition = "2024"

Expand Down
Loading