v.34.1
This commit is contained in:
1 parent
4afa88a65e
commit
2ab44c4c62
49 files changed
+1523
-176
No files matched your search
@@ -0,0 +1,93 @@
|
||||
use super::{audit_tx, Store};
|
||||
use anyhow::{ensure, Context, Result};
|
||||
use serde::Serialize;
|
||||
use sqlx::Row;
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub const API_TOKEN_CAPACITY: i64 = 32;
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ApiTokenMetadata {
|
||||
pub id: String,
|
||||
pub permission: String,
|
||||
pub comment: String,
|
||||
pub allowed_cidrs: Vec<String>,
|
||||
pub created_at: i64,
|
||||
pub last_used_at: Option<i64>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct ApiTokenGrant { pub id: String, pub permission: String }
|
||||
|
||||
impl Store {
|
||||
pub async fn api_tokens(&self) -> Result<Vec<ApiTokenMetadata>> {
|
||||
let rows = sqlx::query("SELECT id,permission,comment,allowed_cidrs,created_at,last_used_at FROM administration_api_tokens ORDER BY created_at DESC,id")
|
||||
.fetch_all(&self.pool).await?;
|
||||
rows.into_iter().map(|row| {
|
||||
let cidrs: String = row.try_get("allowed_cidrs")?;
|
||||
Ok(ApiTokenMetadata { id: row.try_get("id")?, permission: row.try_get("permission")?,
|
||||
comment: row.try_get("comment")?, allowed_cidrs: serde_json::from_str(&cidrs)?,
|
||||
created_at: row.try_get("created_at")?, last_used_at: row.try_get("last_used_at")? })
|
||||
}).collect()
|
||||
}
|
||||
pub async fn create_api_token(&self, id: &str, hash: &str, permission: &str,
|
||||
comment: &str, cidrs: &[String], actor: &str) -> Result<bool> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM administration_api_tokens").fetch_one(&mut *tx).await?;
|
||||
ensure!(count < API_TOKEN_CAPACITY, "API token limit reached");
|
||||
let result = sqlx::query("INSERT OR IGNORE INTO administration_api_tokens(id,token_hash,permission,comment,allowed_cidrs,created_at) VALUES(?,?,?,?,?,?)")
|
||||
.bind(id).bind(hash).bind(permission).bind(comment).bind(serde_json::to_string(cidrs)?)
|
||||
.bind(crate::auth::now()).execute(&mut *tx).await?;
|
||||
if result.rows_affected() != 1 { return Ok(false); }
|
||||
audit_tx(&mut tx, actor, "api_token.create", &format!("id={id} permission={permission}")).await?;
|
||||
tx.commit().await?;
|
||||
Ok(true)
|
||||
}
|
||||
pub async fn revoke_api_token(&self, id: &str, actor: &str) -> Result<bool> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
let result = sqlx::query("DELETE FROM administration_api_tokens WHERE id = ?")
|
||||
.bind(id).execute(&mut *tx).await?;
|
||||
if result.rows_affected() == 0 { return Ok(false); }
|
||||
audit_tx(&mut tx, actor, "api_token.revoke", &format!("id={id}")).await?;
|
||||
tx.commit().await?;
|
||||
Ok(true)
|
||||
}
|
||||
pub async fn verify_api_token(&self, hash: &str, ip: IpAddr) -> Result<Option<ApiTokenGrant>> {
|
||||
let Some(row) = sqlx::query("SELECT id,permission,allowed_cidrs FROM administration_api_tokens WHERE token_hash = ?")
|
||||
.bind(hash).fetch_optional(&self.pool).await? else { return Ok(None); };
|
||||
let cidrs: String = row.try_get("allowed_cidrs")?;
|
||||
let allowed: Vec<String> = serde_json::from_str(&cidrs).context("invalid stored CIDR list")?;
|
||||
if !allowed.iter().filter_map(|s| s.parse::<ipnet::IpNet>().ok()).any(|net| net.contains(&ip)) {
|
||||
return Ok(None);
|
||||
}
|
||||
let id: String = row.try_get("id")?;
|
||||
let permission: String = row.try_get("permission")?;
|
||||
sqlx::query("UPDATE administration_api_tokens SET last_used_at = ? WHERE id = ?")
|
||||
.bind(crate::auth::now()).bind(&id).execute(&self.pool).await?;
|
||||
Ok(Some(ApiTokenGrant { id, permission }))
|
||||
}
|
||||
pub async fn prometheus_token_hash(&self) -> Result<Option<String>> {
|
||||
Ok(sqlx::query_scalar("SELECT token_hash FROM prometheus_credential WHERE id = 1")
|
||||
.fetch_optional(&self.pool).await?)
|
||||
}
|
||||
pub async fn prometheus_token_status(&self) -> Result<Option<i64>> {
|
||||
Ok(sqlx::query_scalar("SELECT created_at FROM prometheus_credential WHERE id = 1 AND token_hash != 'revoked'")
|
||||
.fetch_optional(&self.pool).await?)
|
||||
}
|
||||
pub async fn rotate_prometheus_token(&self, hash: &str, actor: &str) -> Result<()> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
sqlx::query("INSERT INTO prometheus_credential(id,token_hash,created_at) VALUES(1,?,?) ON CONFLICT(id) DO UPDATE SET token_hash=excluded.token_hash,created_at=excluded.created_at")
|
||||
.bind(hash).bind(crate::auth::now()).execute(&mut *tx).await?;
|
||||
audit_tx(&mut tx, actor, "prometheus_token.rotate", "exporter token rotated").await?;
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn revoke_prometheus_token(&self, actor: &str) -> Result<()> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
// Keep a disabled marker so an old environment token does not silently
|
||||
// become valid again after the operator clicks Revoke in the panel.
|
||||
sqlx::query("INSERT INTO prometheus_credential(id,token_hash,created_at) VALUES(1,'revoked',?) ON CONFLICT(id) DO UPDATE SET token_hash='revoked',created_at=excluded.created_at")
|
||||
.bind(crate::auth::now()).execute(&mut *tx).await?;
|
||||
audit_tx(&mut tx, actor, "prometheus_token.revoke", "exporter token revoked").await?;
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user