style(enroll): cargo fmt --all (satisfy CI fmt gate)
All checks were successful
Build and Test / Build Agent (Windows) (pull_request) Successful in 16m35s
Build and Test / Build Server (Linux) (pull_request) Successful in 19m7s
Build and Test / Security Audit (pull_request) Successful in 5m27s
Build and Test / Build Summary (pull_request) Successful in 26s

The Phase A work passed cargo check + clippy + tests locally but missed
`cargo fmt --all -- --check` (the first step of the Linux CI job): module
ordering in db/mod.rs and two trailing-comment alignments in rate_limit.rs.
No logic change. Agent build failure on the prior run was transient infra
(verified: agent crate compiles clean locally).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 10:48:51 -07:00
parent 0f02f23765
commit 4106fc4bc4
5 changed files with 296 additions and 225 deletions

View File

@@ -218,11 +218,7 @@ fn is_collision(existing: &db::machines::Machine, incoming_hostname: &str) -> bo
/// Mint a `cak_`, store its hash bound to `machine_id` + tenant, and return the /// Mint a `cak_`, store its hash bound to `machine_id` + tenant, and return the
/// plaintext. Shared by the new/reuse/move active paths. /// plaintext. Shared by the new/reuse/move active paths.
async fn mint_cak( async fn mint_cak(db: &db::Database, machine_id: Uuid, tenant_id: Uuid) -> ApiResult<String> {
db: &db::Database,
machine_id: Uuid,
tenant_id: Uuid,
) -> ApiResult<String> {
let plaintext = agent_keys::generate_agent_key(); let plaintext = agent_keys::generate_agent_key();
let key_hash = agent_keys::hash_agent_key(&plaintext); let key_hash = agent_keys::hash_agent_key(&plaintext);
db::agent_keys::insert_agent_key(db.pool(), machine_id, &key_hash, Some(tenant_id)) db::agent_keys::insert_agent_key(db.pool(), machine_id, &key_hash, Some(tenant_id))
@@ -287,11 +283,16 @@ pub async fn enroll(
// site_code is not distinguishable by timing (enumeration oracle). // site_code is not distinguishable by timing (enumeration oracle).
equalize_reject_timing(&req.enrollment_key); equalize_reject_timing(&req.enrollment_key);
state.rate_limits.enroll.record_failure(site_code, ip); state.rate_limits.enroll.record_failure(site_code, ip);
audit(db, db::events::EventTypes::ENROLL_REJECTED, ip, json!({ audit(
db,
db::events::EventTypes::ENROLL_REJECTED,
ip,
json!({
"reason": "unknown_site_code", "reason": "unknown_site_code",
"site_code": site_code, "site_code": site_code,
"machine_uid": machine_uid, "machine_uid": machine_uid,
})) }),
)
.await; .await;
tracing::warn!("[ENROLL] unknown site_code={} from {}", site_code, ip); tracing::warn!("[ENROLL] unknown site_code={} from {}", site_code, ip);
// Same opaque rejection shape AND the same KDF cost as a bad key — do not // Same opaque rejection shape AND the same KDF cost as a bad key — do not
@@ -322,13 +323,21 @@ pub async fn enroll(
// has no active key" is not distinguishable by timing from a bad key. // has no active key" is not distinguishable by timing from a bad key.
equalize_reject_timing(&req.enrollment_key); equalize_reject_timing(&req.enrollment_key);
state.rate_limits.enroll.record_failure(site_code, ip); state.rate_limits.enroll.record_failure(site_code, ip);
audit(db, db::events::EventTypes::ENROLL_REJECTED, ip, json!({ audit(
db,
db::events::EventTypes::ENROLL_REJECTED,
ip,
json!({
"reason": "no_active_key", "reason": "no_active_key",
"site_code": site_code, "site_code": site_code,
"machine_uid": machine_uid, "machine_uid": machine_uid,
})) }),
)
.await; .await;
tracing::warn!("[ENROLL] no active enrollment key for site_code={}", site_code); tracing::warn!(
"[ENROLL] no active enrollment key for site_code={}",
site_code
);
return Err(ApiError::new( return Err(ApiError::new(
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
"ENROLL_REJECTED", "ENROLL_REJECTED",
@@ -347,13 +356,22 @@ pub async fn enroll(
if !enrollment_keys::verify_enrollment_key(&req.enrollment_key, &active_key.key_hash) { if !enrollment_keys::verify_enrollment_key(&req.enrollment_key, &active_key.key_hash) {
state.rate_limits.enroll.record_failure(site_code, ip); state.rate_limits.enroll.record_failure(site_code, ip);
audit(db, db::events::EventTypes::ENROLL_REJECTED, ip, json!({ audit(
db,
db::events::EventTypes::ENROLL_REJECTED,
ip,
json!({
"reason": "bad_enrollment_key", "reason": "bad_enrollment_key",
"site_code": site_code, "site_code": site_code,
"machine_uid": machine_uid, "machine_uid": machine_uid,
})) }),
)
.await; .await;
tracing::warn!("[ENROLL] bad enrollment key for site_code={} from {}", site_code, ip); tracing::warn!(
"[ENROLL] bad enrollment key for site_code={} from {}",
site_code,
ip
);
return Err(ApiError::new( return Err(ApiError::new(
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
"ENROLL_REJECTED", "ENROLL_REJECTED",
@@ -366,7 +384,12 @@ pub async fn enroll(
// Build the label/identity params shared by the create/update paths. // Build the label/identity params shared by the create/update paths.
let tags = effective_tags(&req.labels); let tags = effective_tags(&req.labels);
let company = req.labels.company.as_deref().map(str::trim).filter(|s| !s.is_empty()); let company = req
.labels
.company
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty());
let site_label = req let site_label = req
.labels .labels
.site .site
@@ -386,8 +409,13 @@ pub async fn enroll(
let mut attempts = 0u8; let mut attempts = 0u8;
loop { loop {
attempts += 1; attempts += 1;
let existing = let existing = match db::machines::get_machine_by_tenant_uid(
match db::machines::get_machine_by_tenant_uid(db.pool(), tenant_id, machine_uid).await { db.pool(),
tenant_id,
machine_uid,
)
.await
{
Ok(e) => e, Ok(e) => e,
Err(e) => { Err(e) => {
tracing::error!("[ENROLL] DB error on dedup lookup: {}", e); tracing::error!("[ENROLL] DB error on dedup lookup: {}", e);
@@ -416,25 +444,35 @@ pub async fn enroll(
&tags, &tags,
"pending", "pending",
); );
let machine = db::machines::update_enrolled_machine(db.pool(), existing.id, &params) let machine =
db::machines::update_enrolled_machine(db.pool(), existing.id, &params)
.await .await
.map_err(map_update_err)?; .map_err(map_update_err)?;
audit(db, db::events::EventTypes::ENROLL_COLLISION_PENDING, ip, json!({ audit(
db,
db::events::EventTypes::ENROLL_COLLISION_PENDING,
ip,
json!({
"machine_id": machine.id, "machine_id": machine.id,
"machine_uid": machine_uid, "machine_uid": machine_uid,
"site_code": site_code, "site_code": site_code,
"existing_hostname": existing.hostname, "existing_hostname": existing.hostname,
"incoming_hostname": hostname, "incoming_hostname": hostname,
"heuristic": "online_existing_with_different_hostname (PROVISIONAL)", "heuristic": "online_existing_with_different_hostname (PROVISIONAL)",
})) }),
)
.await; .await;
// TODO(SPEC-016): wire to #dev-alerts — collision requires operator // TODO(SPEC-016): wire to #dev-alerts — collision requires operator
// confirmation in the dashboard before this endpoint may activate. // confirmation in the dashboard before this endpoint may activate.
tracing::warn!( tracing::warn!(
"[ENROLL] machine_uid collision -> PENDING: machine_id={} site_code={} \ "[ENROLL] machine_uid collision -> PENDING: machine_id={} site_code={} \
existing_host={} incoming_host={} from {}", existing_host={} incoming_host={} from {}",
machine.id, site_code, existing.hostname, hostname, ip machine.id,
site_code,
existing.hostname,
hostname,
ip
); );
return Ok(( return Ok((
@@ -464,7 +502,11 @@ pub async fn enroll(
// no prior owning site to hijack from. // no prior owning site to hijack from.
if let Some(existing_site) = existing.site_id { if let Some(existing_site) = existing.site_id {
if existing_site != site.id { if existing_site != site.id {
audit(db, db::events::EventTypes::ENROLL_SITE_CONFLICT, ip, json!({ audit(
db,
db::events::EventTypes::ENROLL_SITE_CONFLICT,
ip,
json!({
"machine_id": existing.id, "machine_id": existing.id,
"machine_uid": machine_uid, "machine_uid": machine_uid,
// Record the bound site id for the operator audit trail; the // Record the bound site id for the operator audit trail; the
@@ -472,14 +514,17 @@ pub async fn enroll(
"bound_site_id": existing_site, "bound_site_id": existing_site,
"attempted_site_code": site_code, "attempted_site_code": site_code,
"attempted_site_id": site.id, "attempted_site_id": site.id,
})) }),
)
.await; .await;
// TODO(SPEC-016): wire to #dev-alerts — cross-site enroll refused // TODO(SPEC-016): wire to #dev-alerts — cross-site enroll refused
// (possible accidental move or a cross-site claim attempt). // (possible accidental move or a cross-site claim attempt).
tracing::warn!( tracing::warn!(
"[ENROLL] cross-site conflict REFUSED: machine_id={} already bound to \ "[ENROLL] cross-site conflict REFUSED: machine_id={} already bound to \
a different site; attempted site_code={} from {}", a different site; attempted site_code={} from {}",
existing.id, site_code, ip existing.id,
site_code,
ip
); );
// Opaque-enough: states the machine is already enrolled elsewhere // Opaque-enough: states the machine is already enrolled elsewhere
// and how to move it deliberately, without naming the other site. // and how to move it deliberately, without naming the other site.
@@ -506,21 +551,29 @@ pub async fn enroll(
&tags, &tags,
"active", "active",
); );
let machine = db::machines::update_enrolled_machine(db.pool(), existing.id, &params) let machine =
db::machines::update_enrolled_machine(db.pool(), existing.id, &params)
.await .await
.map_err(map_update_err)?; .map_err(map_update_err)?;
let cak = mint_cak(db, machine.id, tenant_id).await?; let cak = mint_cak(db, machine.id, tenant_id).await?;
audit(db, db::events::EventTypes::ENROLL_REUSE, ip, json!({ audit(
db,
db::events::EventTypes::ENROLL_REUSE,
ip,
json!({
"machine_id": machine.id, "machine_id": machine.id,
"machine_uid": machine_uid, "machine_uid": machine_uid,
"site_code": site_code, "site_code": site_code,
})) }),
)
.await; .await;
tracing::info!( tracing::info!(
"[ENROLL] reuse: machine_id={} re-enrolled at site_code={} from {}", "[ENROLL] reuse: machine_id={} re-enrolled at site_code={} from {}",
machine.id, site_code, ip machine.id,
site_code,
ip
); );
return Ok(( return Ok((
@@ -552,7 +605,8 @@ pub async fn enroll(
&tags, &tags,
"active", "active",
); );
let machine = match db::machines::insert_enrolled_machine(db.pool(), &params).await { let machine = match db::machines::insert_enrolled_machine(db.pool(), &params).await
{
Ok(m) => m, Ok(m) => m,
// TOCTOU loser: a concurrent first-enroll of the same machine_uid won // TOCTOU loser: a concurrent first-enroll of the same machine_uid won
// the race and committed its row between our lookup and this INSERT, so // the race and committed its row between our lookup and this INSERT, so
@@ -564,7 +618,8 @@ pub async fn enroll(
tracing::info!( tracing::info!(
"[ENROLL] concurrent first-enroll race on machine_uid; \ "[ENROLL] concurrent first-enroll race on machine_uid; \
retrying as reuse (site_code={} from {})", retrying as reuse (site_code={} from {})",
site_code, ip site_code,
ip
); );
continue; continue;
} }
@@ -580,17 +635,25 @@ pub async fn enroll(
let cak = mint_cak(db, machine.id, tenant_id).await?; let cak = mint_cak(db, machine.id, tenant_id).await?;
audit(db, db::events::EventTypes::ENROLL_NEW, ip, json!({ audit(
db,
db::events::EventTypes::ENROLL_NEW,
ip,
json!({
"machine_id": machine.id, "machine_id": machine.id,
"machine_uid": machine_uid, "machine_uid": machine_uid,
"site_code": site_code, "site_code": site_code,
"hostname": hostname, "hostname": hostname,
})) }),
)
.await; .await;
// TODO(SPEC-016): wire to #dev-alerts — new-enrollment tripwire. // TODO(SPEC-016): wire to #dev-alerts — new-enrollment tripwire.
tracing::info!( tracing::info!(
"[ENROLL] new: machine_id={} hostname={} site_code={} from {}", "[ENROLL] new: machine_id={} hostname={} site_code={} from {}",
machine.id, hostname, site_code, ip machine.id,
hostname,
site_code,
ip
); );
return Ok(( return Ok((
@@ -636,7 +699,12 @@ fn effective_tags(labels: &EnrollLabels) -> Vec<String> {
.map(|t| t.trim().to_string()) .map(|t| t.trim().to_string())
.filter(|t| !t.is_empty()) .filter(|t| !t.is_empty())
.collect(); .collect();
if let Some(d) = labels.department.as_deref().map(str::trim).filter(|s| !s.is_empty()) { if let Some(d) = labels
.department
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
{
tags.push(format!("department:{}", d)); tags.push(format!("department:{}", d));
} }
if let Some(d) = labels if let Some(d) = labels
@@ -708,7 +776,8 @@ mod tests {
fn timing_equalizer_phc_is_a_valid_parseable_hash() { fn timing_equalizer_phc_is_a_valid_parseable_hash() {
// A wrong password must verify to `false` WITHOUT erroring — proving the PHC // A wrong password must verify to `false` WITHOUT erroring — proving the PHC
// parsed and the KDF actually ran (a parse failure would surface as `Err`). // parsed and the KDF actually ran (a parse failure would surface as `Err`).
let res = crate::auth::password::verify_password("cek_anything_at_all", TIMING_EQUALIZER_PHC); let res =
crate::auth::password::verify_password("cek_anything_at_all", TIMING_EQUALIZER_PHC);
assert!( assert!(
res.is_ok(), res.is_ok(),
"TIMING_EQUALIZER_PHC must be a valid PHC string so the KDF runs; got Err: {:?}", "TIMING_EQUALIZER_PHC must be a valid PHC string so the KDF runs; got Err: {:?}",

View File

@@ -178,7 +178,10 @@ mod tests {
#[test] #[test]
fn fingerprint_differs_per_key() { fn fingerprint_differs_per_key() {
assert_ne!(compute_fingerprint("cek_aaa"), compute_fingerprint("cek_bbb")); assert_ne!(
compute_fingerprint("cek_aaa"),
compute_fingerprint("cek_bbb")
);
} }
#[test] #[test]

View File

@@ -102,9 +102,8 @@ pub async fn rotate_key(
let mut tx = pool.begin().await?; let mut tx = pool.begin().await?;
// Highest existing version for this site (NULL -> 0 so the first key is v1). // Highest existing version for this site (NULL -> 0 so the first key is v1).
let current_max: Option<i32> = sqlx::query_scalar( let current_max: Option<i32> =
"SELECT MAX(version) FROM site_enrollment_keys WHERE site_id = $1", sqlx::query_scalar("SELECT MAX(version) FROM site_enrollment_keys WHERE site_id = $1")
)
.bind(site_id) .bind(site_id)
.fetch_one(&mut *tx) .fetch_one(&mut *tx)
.await?; .await?;

View File

@@ -7,9 +7,9 @@ pub mod agent_keys;
pub mod enrollment_keys; pub mod enrollment_keys;
pub mod events; pub mod events;
pub mod machines; pub mod machines;
pub mod sites;
pub mod releases; pub mod releases;
pub mod sessions; pub mod sessions;
pub mod sites;
pub mod support_codes; pub mod support_codes;
pub mod tenancy; pub mod tenancy;
pub mod users; pub mod users;