From d185d2a4d01cc4cb549ff0d6de35a45e9874df20 Mon Sep 17 00:00:00 2001 From: AB-UK Date: Mon, 29 Dec 2025 02:06:23 +0000 Subject: [PATCH] Added webhook support --- Cargo.lock | 169 ++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 5 +- src/main.rs | 109 ++++++++++++++++++++++------ templates/index.html | 9 +++ 4 files changed, 265 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 639beb6..2dc3f3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,21 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.20" @@ -333,6 +348,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + [[package]] name = "bytes" version = "1.10.1" @@ -361,8 +382,13 @@ version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", + "windows-link 0.1.3", ] [[package]] @@ -943,6 +969,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -1098,6 +1148,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "json-patch" version = "2.0.0" @@ -1800,11 +1860,12 @@ dependencies = [ [[package]] name = "secret-reader" -version = "0.1.1" +version = "0.2.0" dependencies = [ "anyhow", "askama", "axum", + "chrono", "clap", "k8s-openapi", "kube", @@ -2424,12 +2485,116 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "windows-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2479,7 +2644,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", diff --git a/Cargo.toml b/Cargo.toml index d71e8d5..18ec183 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "secret-reader" -version = "0.1.1" -edition = "2021" +version = "0.2.0" +edition = "2024" [dependencies] axum = "0.7" @@ -16,3 +16,4 @@ tracing = "0.1" tracing-subscriber = "0.3" anyhow = "1.0" totp-rs = { version = "5.6", features = ["otpauth"] } +chrono = "0.4" diff --git a/src/main.rs b/src/main.rs index c9db048..d7ac49d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,19 @@ use anyhow::Result; use askama::Template; use axum::{ - extract::{Query, State}, + extract::{Json, Query, State}, http::StatusCode, response::{Html, IntoResponse, Response}, - routing::get, + routing::{get, post}, Router, }; +use chrono; use clap::Parser; use k8s_openapi::api::core::v1::Secret; use kube::{Api, Client}; use serde::{Deserialize, Serialize}; -use std::sync::Arc; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; use totp_rs::TOTP; use tracing::{error, info}; use tracing_subscriber; @@ -27,6 +29,9 @@ struct Args { #[arg(short, long, default_value = "default")] namespace: String, + + #[arg(short = 'w', long, help = "Enable webhook endpoint at /webhook")] + webhook: bool, } #[derive(Clone)] @@ -34,12 +39,24 @@ struct AppState { client: Client, secret_names: Vec, namespace: String, + webhook_secrets: Arc>>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct WebhookSecret { + name: String, + fields: HashMap, + #[serde(skip_deserializing)] + #[serde(default)] + received_at: String, } #[derive(Serialize)] struct SecretData { name: String, data: Vec<(String, String)>, + #[serde(skip_serializing_if = "Option::is_none")] + received_at: Option, } #[derive(Template)] @@ -80,6 +97,7 @@ async fn read_secrets(state: &AppState) -> Result> { result.push(SecretData { name: secret_name.clone(), data: data_pairs, + received_at: None, }); } Err(e) => { @@ -87,6 +105,7 @@ async fn read_secrets(state: &AppState) -> Result> { result.push(SecretData { name: secret_name.clone(), data: vec![("error".to_string(), format!("Failed to read: {}", e))], + received_at: None, }); } } @@ -98,21 +117,8 @@ async fn read_secrets(state: &AppState) -> Result> { async fn index_handler(State(state): State>) -> impl IntoResponse { info!("Handling request, fetching secrets: {:?}", state.secret_names); - match read_secrets(&state).await { - Ok(secrets) => { - let template = IndexTemplate { - secrets, - error: None, - }; - - match template.render() { - Ok(html) => Html(html).into_response(), - Err(e) => { - error!("Template render error: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "Template render error").into_response() - } - } - } + let mut all_secrets = match read_secrets(&state).await { + Ok(secrets) => secrets, Err(e) => { error!("Failed to read secrets: {}", e); let template = IndexTemplate { @@ -120,10 +126,39 @@ async fn index_handler(State(state): State>) -> impl IntoResponse error: Some(format!("Failed to read secrets: {}", e)), }; - match template.render() { + return match template.render() { Ok(html) => Html(html).into_response(), Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to read secrets").into_response(), - } + }; + } + }; + + // Add webhook secrets + if let Ok(webhook_secrets) = state.webhook_secrets.read() { + for (_, webhook_secret) in webhook_secrets.iter() { + let mut data_pairs: Vec<(String, String)> = webhook_secret.fields.iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + data_pairs.sort_by(|a, b| a.0.cmp(&b.0)); + + all_secrets.push(SecretData { + name: webhook_secret.name.clone(), + data: data_pairs, + received_at: Some(webhook_secret.received_at.clone()), + }); + } + } + + let template = IndexTemplate { + secrets: all_secrets, + error: None, + }; + + match template.render() { + Ok(html) => Html(html).into_response(), + Err(e) => { + error!("Template render error: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, "Template render error").into_response() } } } @@ -132,6 +167,27 @@ async fn health_handler() -> impl IntoResponse { "OK" } +async fn webhook_handler( + State(state): State>, + Json(payload): Json, +) -> impl IntoResponse { + info!("Received webhook for secret: {}", payload.name); + + let mut webhook_secret = payload; + webhook_secret.received_at = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(); + + match state.webhook_secrets.write() { + Ok(mut secrets) => { + secrets.insert(webhook_secret.name.clone(), webhook_secret); + (StatusCode::OK, "Webhook received") + } + Err(e) => { + error!("Failed to write webhook secret: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, "Failed to store webhook") + } + } +} + fn generate_totp_code(otpauth_url: &str) -> Option { // Try to parse the otpauth URL directly using totp-rs match TOTP::from_url(otpauth_url) { @@ -220,13 +276,20 @@ async fn main() -> Result<()> { client, secret_names: args.secrets, namespace: args.namespace, + webhook_secrets: Arc::new(RwLock::new(HashMap::new())), }); - let app = Router::new() + let mut app = Router::new() .route("/", get(index_handler)) .route("/health", get(health_handler)) - .route("/secret", get(secret_handler)) - .with_state(state); + .route("/secret", get(secret_handler)); + + if args.webhook { + info!("Webhook endpoint enabled at /webhook"); + app = app.route("/webhook", post(webhook_handler)); + } + + let app = app.with_state(state.clone()); let addr = format!("0.0.0.0:{}", args.port); info!("Server listening on {}", addr); diff --git a/templates/index.html b/templates/index.html index 3914f50..0d98157 100644 --- a/templates/index.html +++ b/templates/index.html @@ -90,6 +90,12 @@ color: #666; margin-left: 10px; } + .received-at { + font-size: 11px; + color: #888; + margin-top: 5px; + font-style: italic; + } .error { background: #ffdddd; color: #cc0000; @@ -125,6 +131,9 @@ {% endfor %} + {% if let Some(received) = secret.received_at %} +
Received: {{ received }}
+ {% endif %} {% endfor %} {% endif %}