This commit is contained in:
Mateusz Gruszczyński committed 2026-10-12 14:57:37 +02:00
1 parent 180a0e4877
commit d23fa5e154
39 files changed
+1521 -376

No files matched your search

+139 -36
View File
@@ -5,6 +5,41 @@ use super::{
use anyhow::{ensure, Context, Result};
use sqlx::Row;
fn normalized_certificate_api_ids(comment: &str, certificate_ids: &[String]) -> Result<Vec<String>> {
ensure!(
comment.len() <= 256 && !comment.chars().any(char::is_control),
"invalid certificate API comment"
);
ensure!(
!certificate_ids.is_empty() && certificate_ids.len() <= 32,
"certificate API token must allow between 1 and 32 certificate ids"
);
let mut normalized = certificate_ids.to_vec();
normalized.sort();
normalized.dedup();
ensure!(
normalized.iter().all(|value| value != "*" && crate::config::valid_id(value)),
"invalid certificate API certificate id"
);
ensure!(
serde_json::to_string(&normalized)?.len() <= 4096,
"certificate API certificate id list is too large"
);
Ok(normalized)
}
fn credential_from_row(row: sqlx::sqlite::SqliteRow) -> Result<CertificateApiCredential> {
let raw_ids: String = row.try_get("certificate_ids")?;
Ok(CertificateApiCredential {
key_id: row.try_get("key_id")?,
comment: row.try_get("comment")?,
created_at: row.try_get("created_at")?,
last_used_at: row.try_get("last_used_at")?,
allow_deploy: row.try_get::<i64, _>("allow_deploy")? != 0,
certificate_ids: serde_json::from_str(&raw_ids).context("invalid certificate API grant ids")?,
})
}
impl Store {
pub async fn certificate_api_credentials(&self) -> Result<Vec<CertificateApiCredential>> {
let rows = sqlx::query(
@@ -17,21 +52,7 @@ impl Store {
)
.fetch_all(&self.pool)
.await?;
rows.into_iter()
.map(|row| {
let raw_ids: String = row.try_get("certificate_ids")?;
let certificate_ids: Vec<String> =
serde_json::from_str(&raw_ids).context("invalid certificate API grant ids")?;
Ok(CertificateApiCredential {
key_id: row.try_get("key_id")?,
comment: row.try_get("comment")?,
created_at: row.try_get("created_at")?,
last_used_at: row.try_get("last_used_at")?,
allow_deploy: row.try_get::<i64, _>("allow_deploy")? != 0,
certificate_ids,
})
})
.collect()
rows.into_iter().map(credential_from_row).collect()
}
pub async fn upsert_certificate_api_credential(
@@ -51,28 +72,8 @@ impl Store {
token_hash.len() == 64 && token_hash.bytes().all(|b| b.is_ascii_hexdigit()),
"invalid certificate API token hash"
);
ensure!(
comment.len() <= 256 && !comment.chars().any(char::is_control),
"invalid certificate API comment"
);
ensure!(
!certificate_ids.is_empty() && certificate_ids.len() <= 32,
"certificate API token must allow between 1 and 32 certificate ids"
);
let mut normalized_ids = certificate_ids.to_vec();
normalized_ids.sort();
normalized_ids.dedup();
ensure!(
normalized_ids
.iter()
.all(|value| value != "*" && crate::config::valid_id(value)),
"invalid certificate API certificate id"
);
let normalized_ids = normalized_certificate_api_ids(comment, certificate_ids)?;
let certificate_ids_json = serde_json::to_string(&normalized_ids)?;
ensure!(
certificate_ids_json.len() <= 4096,
"certificate API certificate id list is too large"
);
let now = crate::auth::now();
let mut tx = self.pool.begin().await?;
let exists: i64 =
@@ -134,6 +135,108 @@ impl Store {
})
}
// Metadata access never selects token_hash, including for a single credential.
pub async fn certificate_api_credential(
&self,
key_id: &str,
) -> Result<Option<CertificateApiCredential>> {
let row = sqlx::query(
"SELECT c.key_id, c.comment, c.created_at, c.last_used_at, \
COALESCE(g.allow_deploy, 0) AS allow_deploy, \
COALESCE(g.certificate_ids, '[\"*\"]') AS certificate_ids \
FROM certificate_api_credentials c \
LEFT JOIN certificate_api_grants g ON g.key_id = c.key_id \
WHERE c.key_id = ?",
)
.bind(key_id)
.fetch_optional(&self.pool)
.await?;
row.map(credential_from_row).transpose()
}
// Do not touch token_hash, created_at, or last_used_at while editing settings.
pub async fn edit_certificate_api_credential(
&self,
key_id: &str,
comment: &str,
allow_deploy: bool,
certificate_ids: &[String],
actor: &str,
) -> Result<Option<CertificateApiCredential>> {
ensure!(
(3..=64).contains(&key_id.len()) && crate::config::valid_id(key_id),
"invalid certificate API key id"
);
let normalized_ids = normalized_certificate_api_ids(comment, certificate_ids)?;
let ids_json = serde_json::to_string(&normalized_ids)?;
let mut tx = self.pool.begin().await?;
let changed = sqlx::query("UPDATE certificate_api_credentials SET comment = ? WHERE key_id = ?")
.bind(comment)
.bind(key_id)
.execute(&mut *tx)
.await?;
if changed.rows_affected() == 0 {
return Ok(None);
}
sqlx::query(
"INSERT INTO certificate_api_grants(key_id, allow_deploy, certificate_ids) VALUES(?, ?, ?) \
ON CONFLICT(key_id) DO UPDATE SET allow_deploy=excluded.allow_deploy, certificate_ids=excluded.certificate_ids",
)
.bind(key_id)
.bind(if allow_deploy { 1_i64 } else { 0_i64 })
.bind(&ids_json)
.execute(&mut *tx)
.await?;
audit_tx(
&mut tx,
actor,
"certificate_api.edit",
&format!(
"key_id={key_id}; scope={}; certificate_ids={}",
if allow_deploy { "deploy+reload" } else { "reload" },
normalized_ids.join(",")
),
)
.await?;
tx.commit().await?;
self.certificate_api_credential(key_id).await
}
// Hash-only rotation preserves comment/grants and invalidates the old token at commit.
pub async fn regenerate_certificate_api_token(
&self,
key_id: &str,
token_hash: &str,
actor: &str,
) -> Result<Option<CertificateApiCredential>> {
ensure!(
(3..=64).contains(&key_id.len()) && crate::config::valid_id(key_id),
"invalid certificate API key id"
);
ensure!(
token_hash.len() == 64 && token_hash.bytes().all(|b| b.is_ascii_hexdigit()),
"invalid certificate API token hash"
);
let now = crate::auth::now();
let mut tx = self.pool.begin().await?;
let changed = sqlx::query(
"UPDATE certificate_api_credentials \
SET token_hash = ?, created_at = ?, last_used_at = NULL WHERE key_id = ?",
)
.bind(token_hash)
.bind(now)
.bind(key_id)
.execute(&mut *tx)
.await?;
if changed.rows_affected() == 0 {
return Ok(None);
}
audit_tx(&mut tx, actor, "certificate_api.regenerate", &format!("key_id={key_id}"))
.await?;
tx.commit().await?;
self.certificate_api_credential(key_id).await
}
pub async fn revoke_certificate_api_credential(
&self,
key_id: &str,