Server-side zero-touch per-site enrollment (Phase A: backend + DB only;
agent-side machine_uid derivation is Phase B, server treats it as opaque).
Migration 010_spec016_enrollment.sql:
- connect_sites: relational site anchor (site_code natural key, per-tenant
unique). The spec assumed a sites table existed; it did not (site/company
were free-text columns on connect_machines), so this creates a minimal one.
- site_enrollment_keys: rotatable, Argon2id-hashed cek_ secret + monotonic
version + hex fingerprint + active flag; one-active-per-site partial unique.
- connect_machines: + site_id (FK), + enrollment_state ('active'|'pending')
collision gate, + per-tenant (tenant_id, machine_uid) unique index added
ALONGSIDE the 008 global index (the connect-path upsert_machine ON CONFLICT
arbiter binds to 008 — dropping it would break live reconnect).
- connect_sites.enrollment_policy: reserved (default auto-approve), not enforced.
auth/enrollment_keys.rs: cek_ mint (256-bit, OS CSPRNG), Argon2id hash/verify
(reuses auth::password), and hex fingerprint vN (XXXX) per resolved-decision #3.
db/sites.rs + db/enrollment_keys.rs: runtime sqlx persistence; rotate_key
deactivates+inserts in one tx to hold the one-active-key invariant.
POST /api/enroll (public, api/enroll.rs): site_code+cek_ verify against active
key -> dedup on (tenant, machine_uid) -> new / reuse / site-move / collision.
Collision gate (PROVISIONAL heuristic: online existing row + different hostname)
-> pending, no usable cak_, alert. Mints cak_ via existing agent_keys path in the
exact form relay::validate_agent_api_key expects. Per-(site_code,IP) rate-limit +
lockout (EnrollLimiter). Audit events + [ENROLL] alert markers with
TODO(SPEC-016) #dev-alerts notes.
Admin (JWT) api/sites.rs: POST /api/sites/:id/enrollment-key/rotate (plaintext +
fingerprint once) and GET .../enrollment-key (fingerprint/version, no secret).
Routes wired in main.rs (enroll public, rotation admin). 13 new unit tests;
full server suite 99 passing. cargo check + clippy clean on the host (Windows)
target — Linux cross-target not installed here; server crate is platform-neutral
Rust. No sqlx offline cache needed (codebase uses runtime queries, no query!).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
57 lines
1.3 KiB
Rust
57 lines
1.3 KiB
Rust
//! Database module for GuruConnect
|
|
//!
|
|
//! Handles persistence for machines, sessions, and audit logging.
|
|
//! Optional - server works without database if DATABASE_URL not set.
|
|
|
|
pub mod agent_keys;
|
|
pub mod enrollment_keys;
|
|
pub mod events;
|
|
pub mod machines;
|
|
pub mod sites;
|
|
pub mod releases;
|
|
pub mod sessions;
|
|
pub mod support_codes;
|
|
pub mod tenancy;
|
|
pub mod users;
|
|
|
|
use anyhow::Result;
|
|
use sqlx::postgres::PgPoolOptions;
|
|
use sqlx::PgPool;
|
|
use tracing::info;
|
|
|
|
pub use releases::*;
|
|
pub use users::*;
|
|
|
|
/// Database connection pool wrapper
|
|
#[derive(Clone)]
|
|
pub struct Database {
|
|
pool: PgPool,
|
|
}
|
|
|
|
impl Database {
|
|
/// Initialize database connection pool
|
|
pub async fn connect(database_url: &str, max_connections: u32) -> Result<Self> {
|
|
info!("Connecting to database...");
|
|
let pool = PgPoolOptions::new()
|
|
.max_connections(max_connections)
|
|
.connect(database_url)
|
|
.await?;
|
|
|
|
info!("Database connection established");
|
|
Ok(Self { pool })
|
|
}
|
|
|
|
/// Run database migrations
|
|
pub async fn migrate(&self) -> Result<()> {
|
|
info!("Running database migrations...");
|
|
sqlx::migrate!("./migrations").run(&self.pool).await?;
|
|
info!("Migrations complete");
|
|
Ok(())
|
|
}
|
|
|
|
/// Get reference to the connection pool
|
|
pub fn pool(&self) -> &PgPool {
|
|
&self.pool
|
|
}
|
|
}
|