Implement filters and column management in admin UI lists
build / build-linux-amd64 (push) Successful in 1m52s
build / build-linux-amd64 (push) Successful in 1m52s
This commit is contained in:
+109
-13
@@ -74,7 +74,15 @@ pub async fn verify(
|
||||
// Partial headers: someone tried to sign but messed up the request.
|
||||
// Don't fall through to legacy — treat as an outright failure so we
|
||||
// don't silently downgrade a misconfigured agent.
|
||||
_ => return Err(ApiError::Unauthorized),
|
||||
_ => {
|
||||
hbb_common::log::warn!(
|
||||
"signed API {}: partial headers (id={:?}, sig_present={})",
|
||||
path,
|
||||
id_hdr,
|
||||
sig_hdr.map(|s| !s.is_empty()).unwrap_or(false),
|
||||
);
|
||||
return Err(ApiError::Unauthorized);
|
||||
}
|
||||
};
|
||||
|
||||
// Parse "v1.<ts>.<b64>".
|
||||
@@ -83,14 +91,40 @@ pub async fn verify(
|
||||
let ts_s = parts.next().unwrap_or("");
|
||||
let sig_b64 = parts.next().unwrap_or("");
|
||||
if ver != SIG_VERSION || ts_s.is_empty() || sig_b64.is_empty() {
|
||||
hbb_common::log::warn!(
|
||||
"signed API {} from {}: malformed signature header (ver={:?})",
|
||||
path, id_hdr, ver,
|
||||
);
|
||||
return Err(ApiError::Unauthorized);
|
||||
}
|
||||
let ts: i64 = ts_s.parse().map_err(|_| ApiError::Unauthorized)?;
|
||||
let ts: i64 = match ts_s.parse() {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
hbb_common::log::warn!(
|
||||
"signed API {} from {}: bad timestamp {:?}",
|
||||
path, id_hdr, ts_s,
|
||||
);
|
||||
return Err(ApiError::Unauthorized);
|
||||
}
|
||||
};
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
if (now - ts).abs() > SKEW_TOLERANCE_SECS {
|
||||
hbb_common::log::warn!(
|
||||
"signed API {} from {}: clock skew {}s exceeds {}s tolerance",
|
||||
path, id_hdr, (now - ts).abs(), SKEW_TOLERANCE_SECS,
|
||||
);
|
||||
return Err(ApiError::Unauthorized);
|
||||
}
|
||||
let sig_bytes = base64::decode(sig_b64).map_err(|_| ApiError::Unauthorized)?;
|
||||
let sig_bytes = match base64::decode(sig_b64) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
hbb_common::log::warn!(
|
||||
"signed API {} from {}: base64 decode failed: {}",
|
||||
path, id_hdr, e,
|
||||
);
|
||||
return Err(ApiError::Unauthorized);
|
||||
}
|
||||
};
|
||||
|
||||
// Replay check before the expensive crypto. The (id, ts, sig-prefix)
|
||||
// tuple is unique per request from a non-broken agent.
|
||||
@@ -102,6 +136,10 @@ pub async fn verify(
|
||||
let mut cache = REPLAY.lock().unwrap();
|
||||
cache.retain(|_, exp| *exp > now);
|
||||
if cache.contains_key(&replay_key) {
|
||||
hbb_common::log::warn!(
|
||||
"signed API {} from {}: replay rejected",
|
||||
path, id_hdr,
|
||||
);
|
||||
return Err(ApiError::Unauthorized);
|
||||
}
|
||||
if cache.len() < REPLAY_CACHE_MAX {
|
||||
@@ -112,15 +150,41 @@ pub async fn verify(
|
||||
}
|
||||
|
||||
// Look up the peer's pk and managed flag in one query.
|
||||
let (pk_bytes, managed) = state
|
||||
let row = state
|
||||
.db
|
||||
.peer_get_auth(id_hdr)
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(e.to_string()))?
|
||||
.ok_or(ApiError::Unauthorized)?;
|
||||
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||
let (pk_bytes, managed) = match row {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
// Early-boot race: the agent generates its keypair and starts
|
||||
// signing API requests before its `--server` child has done
|
||||
// the rendezvous RegisterPk handshake that creates the peer
|
||||
// row. Returning Unauthorized here would leave brand-new
|
||||
// agents stuck — the retry loop is designed around the
|
||||
// ID_NOT_FOUND response from the handler, not a hard auth
|
||||
// failure. Fall through to legacy so the handler can answer
|
||||
// ID_NOT_FOUND; the next retry after RegisterPk completes
|
||||
// will validate normally and TOFU-promote.
|
||||
hbb_common::log::debug!(
|
||||
"signed API request for unregistered peer {} — pre-rendezvous race, \
|
||||
deferring to legacy path",
|
||||
id_hdr,
|
||||
);
|
||||
return Ok(AuthOutcome::LegacyUnsigned);
|
||||
}
|
||||
};
|
||||
if pk_bytes.is_empty() {
|
||||
// No PK registered — rendezvous hasn't completed. Can't verify.
|
||||
return Err(ApiError::Unauthorized);
|
||||
// Peer row exists (rendezvous touched it) but no PK yet — same
|
||||
// race as above, mid-handshake. Defer to legacy; the handler's
|
||||
// `enforce_managed_for_id` still protects this peer if it was
|
||||
// somehow flagged managed=1 with no pk.
|
||||
hbb_common::log::debug!(
|
||||
"signed API request for peer {} with empty pk — deferring to legacy path",
|
||||
id_hdr,
|
||||
);
|
||||
return Ok(AuthOutcome::LegacyUnsigned);
|
||||
}
|
||||
|
||||
// Build the canonical signed message:
|
||||
@@ -136,11 +200,37 @@ pub async fn verify(
|
||||
msg.push(b'\n');
|
||||
msg.extend_from_slice(body_sha.as_ref());
|
||||
|
||||
let pk = sodiumoxide::crypto::sign::PublicKey::from_slice(&pk_bytes)
|
||||
.ok_or(ApiError::Unauthorized)?;
|
||||
let sig = sodiumoxide::crypto::sign::Signature::from_bytes(&sig_bytes)
|
||||
.map_err(|_| ApiError::Unauthorized)?;
|
||||
let pk = match sodiumoxide::crypto::sign::PublicKey::from_slice(&pk_bytes) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
hbb_common::log::warn!(
|
||||
"signed API {} from {}: stored pk ({}B) is not a valid Ed25519 public key",
|
||||
path, id_hdr, pk_bytes.len(),
|
||||
);
|
||||
return Err(ApiError::Unauthorized);
|
||||
}
|
||||
};
|
||||
let sig = match sodiumoxide::crypto::sign::Signature::from_bytes(&sig_bytes) {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
hbb_common::log::warn!(
|
||||
"signed API {} from {}: signature length {} is not the Ed25519 size",
|
||||
path, id_hdr, sig_bytes.len(),
|
||||
);
|
||||
return Err(ApiError::Unauthorized);
|
||||
}
|
||||
};
|
||||
if !sodiumoxide::crypto::sign::verify_detached(&sig, &msg, &pk) {
|
||||
// The agent's keypair doesn't match the pk stored in `peer`.
|
||||
// Usually this means a config was wiped/regenerated on the agent
|
||||
// side without the server's row being cleared — the next
|
||||
// successful RegisterPk handshake will fix it.
|
||||
hbb_common::log::warn!(
|
||||
"signed API {} from {}: signature does NOT verify against stored pk \
|
||||
(agent's keypair differs from the one rendezvous registered) \
|
||||
— managed={}",
|
||||
path, id_hdr, managed,
|
||||
);
|
||||
return Err(ApiError::Unauthorized);
|
||||
}
|
||||
|
||||
@@ -178,7 +268,13 @@ pub async fn enforce_managed_for_id(
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||
match row {
|
||||
Some((_, true)) => Err(ApiError::Unauthorized),
|
||||
Some((_, true)) => {
|
||||
hbb_common::log::warn!(
|
||||
"rejecting unsigned API request for managed peer {}",
|
||||
id,
|
||||
);
|
||||
Err(ApiError::Unauthorized)
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user