Skip to main content

orchestrator_security/
lib.rs

1//! SecretStore encryption, key lifecycle, audit, and secure file helpers.
2//!
3//! This crate provides the security primitives used by the agent orchestrator
4//! for encrypting/decrypting SecretStore values, managing key rotation, emitting
5//! audit events, and creating files/directories with safe permissions.
6
7#![deny(missing_docs)]
8
9/// SecretStore key audit event types and database helpers.
10pub mod secret_key_audit;
11/// SecretStore key lifecycle state machine and rotation logic.
12pub mod secret_key_lifecycle;
13/// SecretStore encryption/decryption helpers (AES-256-GCM-SIV envelope scheme).
14pub mod secret_store_crypto;
15/// Secure file and directory creation helpers.
16pub mod secure_files;
17
18/// Initializes the minimal database schema required by security tests.
19///
20/// Creates the `secret_keys`, `secret_key_audit`, and `resources` tables
21/// needed by lifecycle and crypto integration tests without pulling in
22/// the full persistence bootstrap.
23#[cfg(test)]
24pub(crate) fn init_test_schema(db_path: &std::path::Path) -> anyhow::Result<()> {
25    let conn = open_conn(db_path)?;
26    conn.execute_batch(
27        r#"
28        CREATE TABLE IF NOT EXISTS schema_version (
29            version INTEGER NOT NULL
30        );
31        INSERT INTO schema_version (version) VALUES (99);
32
33        CREATE TABLE IF NOT EXISTS secret_keys (
34            key_id TEXT PRIMARY KEY,
35            state TEXT NOT NULL,
36            fingerprint TEXT NOT NULL,
37            file_path TEXT NOT NULL,
38            created_at TEXT NOT NULL,
39            activated_at TEXT,
40            rotated_out_at TEXT,
41            retired_at TEXT,
42            revoked_at TEXT
43        );
44        CREATE INDEX IF NOT EXISTS idx_secret_keys_state ON secret_keys(state);
45
46        CREATE TABLE IF NOT EXISTS secret_key_audit (
47            id INTEGER PRIMARY KEY AUTOINCREMENT,
48            event_kind TEXT NOT NULL,
49            key_id TEXT NOT NULL,
50            key_fingerprint TEXT NOT NULL,
51            actor TEXT NOT NULL,
52            detail_json TEXT NOT NULL DEFAULT '{}',
53            created_at TEXT NOT NULL
54        );
55        CREATE INDEX IF NOT EXISTS idx_secret_key_audit_created ON secret_key_audit(created_at);
56        CREATE INDEX IF NOT EXISTS idx_secret_key_audit_key_id ON secret_key_audit(key_id, created_at);
57
58        CREATE TABLE IF NOT EXISTS resources (
59            kind TEXT NOT NULL,
60            project TEXT NOT NULL,
61            name TEXT NOT NULL,
62            api_version TEXT NOT NULL,
63            spec_json TEXT NOT NULL,
64            metadata_json TEXT NOT NULL,
65            generation INTEGER NOT NULL DEFAULT 1,
66            created_at TEXT NOT NULL,
67            updated_at TEXT NOT NULL,
68            PRIMARY KEY (kind, project, name)
69        );
70
71        CREATE TABLE IF NOT EXISTS resource_versions (
72            id INTEGER PRIMARY KEY AUTOINCREMENT,
73            kind TEXT NOT NULL,
74            project TEXT NOT NULL,
75            name TEXT NOT NULL,
76            spec_json TEXT NOT NULL,
77            metadata_json TEXT NOT NULL DEFAULT '{}',
78            version INTEGER NOT NULL,
79            author TEXT NOT NULL DEFAULT '',
80            created_at TEXT NOT NULL
81        );
82        CREATE INDEX IF NOT EXISTS idx_resource_versions_lookup
83            ON resource_versions(kind, project, name, version DESC);
84        "#,
85    )?;
86    Ok(())
87}
88
89/// Returns the current UTC timestamp as an RFC 3339 string.
90pub(crate) fn now_ts() -> String {
91    chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
92}
93
94/// Opens a SQLite connection and applies standard busy-timeout and pragma settings.
95pub(crate) fn open_conn(db_path: &std::path::Path) -> anyhow::Result<rusqlite::Connection> {
96    use anyhow::Context;
97    let conn =
98        rusqlite::Connection::open(db_path).context("failed to open sqlite db")?;
99    conn.busy_timeout(std::time::Duration::from_millis(5000))
100        .context("failed to set sqlite busy timeout")?;
101    conn.execute_batch("PRAGMA foreign_keys = ON;")
102        .context("failed to configure sqlite pragmas")?;
103    Ok(conn)
104}