v0,28,2
This commit is contained in:
1 parent
d940f9995f
commit
65e082b9d1
61 files changed
+1225
-581
No files matched your search
@@ -0,0 +1,217 @@
|
||||
use super::{
|
||||
audit_tx, CertificateApiCredential, CertificateApiGrant, Store,
|
||||
CERTIFICATE_API_CREDENTIAL_CAPACITY,
|
||||
};
|
||||
use anyhow::{ensure, Context, Result};
|
||||
use sqlx::Row;
|
||||
|
||||
impl Store {
|
||||
pub async fn certificate_api_credentials(&self) -> Result<Vec<CertificateApiCredential>> {
|
||||
let rows = 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 \
|
||||
ORDER BY c.key_id",
|
||||
)
|
||||
.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()
|
||||
}
|
||||
|
||||
pub async fn upsert_certificate_api_credential(
|
||||
&self,
|
||||
key_id: &str,
|
||||
token_hash: &str,
|
||||
comment: &str,
|
||||
allow_deploy: bool,
|
||||
certificate_ids: &[String],
|
||||
actor: &str,
|
||||
) -> Result<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"
|
||||
);
|
||||
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 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 =
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM certificate_api_credentials WHERE key_id = ?")
|
||||
.bind(key_id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
if exists == 0 {
|
||||
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM certificate_api_credentials")
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
ensure!(
|
||||
count < CERTIFICATE_API_CREDENTIAL_CAPACITY,
|
||||
"certificate API credential limit reached"
|
||||
);
|
||||
}
|
||||
sqlx::query(
|
||||
"INSERT INTO certificate_api_credentials(key_id, token_hash, comment, created_at, last_used_at) VALUES(?, ?, ?, ?, NULL) \
|
||||
ON CONFLICT(key_id) DO UPDATE SET token_hash=excluded.token_hash, comment=excluded.comment, created_at=excluded.created_at, last_used_at=NULL",
|
||||
)
|
||||
.bind(key_id)
|
||||
.bind(token_hash)
|
||||
.bind(comment)
|
||||
.bind(now)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
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(&certificate_ids_json)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
audit_tx(
|
||||
&mut tx,
|
||||
actor,
|
||||
"certificate_api.rotate",
|
||||
&format!(
|
||||
"key_id={key_id}; scope={}; certificate_ids={}",
|
||||
if allow_deploy {
|
||||
"deploy+reload"
|
||||
} else {
|
||||
"reload"
|
||||
},
|
||||
normalized_ids.join(",")
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(CertificateApiCredential {
|
||||
key_id: key_id.to_owned(),
|
||||
comment: comment.to_owned(),
|
||||
created_at: now,
|
||||
last_used_at: None,
|
||||
allow_deploy,
|
||||
certificate_ids: normalized_ids,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn revoke_certificate_api_credential(
|
||||
&self,
|
||||
key_id: &str,
|
||||
actor: &str,
|
||||
) -> Result<bool> {
|
||||
ensure!(
|
||||
(3..=64).contains(&key_id.len()) && crate::config::valid_id(key_id),
|
||||
"invalid certificate API key id"
|
||||
);
|
||||
let mut tx = self.pool.begin().await?;
|
||||
let result = sqlx::query("DELETE FROM certificate_api_credentials WHERE key_id = ?")
|
||||
.bind(key_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
if result.rows_affected() > 0 {
|
||||
audit_tx(
|
||||
&mut tx,
|
||||
actor,
|
||||
"certificate_api.revoke",
|
||||
&format!("key_id={key_id}"),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
pub async fn revoke_all_certificate_api_credentials(&self, actor: &str) -> Result<u64> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
let result = sqlx::query("DELETE FROM certificate_api_credentials")
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
if result.rows_affected() > 0 {
|
||||
audit_tx(
|
||||
&mut tx,
|
||||
actor,
|
||||
"certificate_api.revoke_all",
|
||||
&format!("count={}", result.rows_affected()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
pub async fn verify_certificate_api_credential(
|
||||
&self,
|
||||
key_id: &str,
|
||||
token_hash: &str,
|
||||
) -> Result<Option<CertificateApiGrant>> {
|
||||
let row = sqlx::query(
|
||||
"SELECT c.token_hash, 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?;
|
||||
let Some(row) = row else {
|
||||
return Ok(None);
|
||||
};
|
||||
let expected: String = row.try_get("token_hash")?;
|
||||
if !crate::auth::equal_secret(token_hash, &expected) {
|
||||
return Ok(None);
|
||||
}
|
||||
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")?;
|
||||
sqlx::query("UPDATE certificate_api_credentials SET last_used_at = ? WHERE key_id = ?")
|
||||
.bind(crate::auth::now())
|
||||
.bind(key_id)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(Some(CertificateApiGrant {
|
||||
allow_deploy: row.try_get::<i64, _>("allow_deploy")? != 0,
|
||||
certificate_ids,
|
||||
}))
|
||||
}
|
||||
}
|
||||
+21
-5
@@ -105,12 +105,21 @@ mod tests {
|
||||
let token = "b".repeat(64);
|
||||
let hash = crate::auth::secret_hash(&token);
|
||||
let saved = store
|
||||
.upsert_certificate_api_credential("certbot-main", &hash, "production certbot", "owner")
|
||||
.upsert_certificate_api_credential(
|
||||
"certbot-main",
|
||||
&hash,
|
||||
"production certbot",
|
||||
false,
|
||||
&["shared-cert".to_owned()],
|
||||
"owner",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(saved.key_id, "certbot-main");
|
||||
assert_eq!(saved.comment, "production certbot");
|
||||
assert!(saved.last_used_at.is_none());
|
||||
assert!(!saved.allow_deploy);
|
||||
assert_eq!(saved.certificate_ids, vec!["shared-cert"]);
|
||||
|
||||
let raw: String = sqlx::query_scalar(
|
||||
"SELECT token_hash FROM certificate_api_credentials WHERE key_id = ?",
|
||||
@@ -128,23 +137,30 @@ mod tests {
|
||||
"deploy-hook",
|
||||
&second_hash,
|
||||
"secondary deploy hook",
|
||||
true,
|
||||
&["deploy-cert".to_owned()],
|
||||
"owner",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(store.certificate_api_credentials().await.unwrap().len(), 2);
|
||||
|
||||
assert!(store
|
||||
let grant = store
|
||||
.verify_certificate_api_credential("certbot-main", &hash)
|
||||
.await
|
||||
.unwrap());
|
||||
assert!(!store
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(!grant.allow_deploy);
|
||||
assert!(grant.allows_certificate("shared-cert"));
|
||||
assert!(!grant.allows_certificate("other-cert"));
|
||||
assert!(store
|
||||
.verify_certificate_api_credential(
|
||||
"certbot-main",
|
||||
&crate::auth::secret_hash(&"c".repeat(64)),
|
||||
)
|
||||
.await
|
||||
.unwrap());
|
||||
.unwrap()
|
||||
.is_none());
|
||||
let credentials = store.certificate_api_credentials().await.unwrap();
|
||||
assert!(credentials
|
||||
.iter()
|
||||
|
||||
Reference in new issue
Block a user