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