kych

OAuth 2.0 API for Swiyu to enable Taler integration of Swiyu for KYC (experimental)
Log | Files | Refs | README

commit 441b37187fed12cadc53f3579d156083e94a8215
parent 07f18ff7bf19595900a2921d47d6d57f1abdadf6
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date:   Mon, 19 Jan 2026 19:15:43 +0100

Unify config and disable webhook worker

Unify configuration by switching to a single [kych-oauth2-gateway] section with
uppercase keys and integrated client_* sections, disable compilation of the
webhook worker binary while keeping its source, and update the client management
CLI to match the new config format; additionally make UNIX socket permissions
configurable via UNIXPATH_MODE (default 0666), adjust related logging, and
replace the old example configs with a unified kych.conf.example.

Diffstat:
Mkych_oauth2_gateway/Cargo.toml | 5+----
Dkych_oauth2_gateway/clients.conf.example | 22----------------------
Dkych_oauth2_gateway/config.ini.example | 19-------------------
Mkych_oauth2_gateway/src/bin/client_management_cli.rs | 43++++++++++++++++++++++---------------------
Mkych_oauth2_gateway/src/config.rs | 167++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mkych_oauth2_gateway/src/lib.rs | 5++---
Mkych_oauth2_gateway/src/main.rs | 10+++++-----
7 files changed, 123 insertions(+), 148 deletions(-)

diff --git a/kych_oauth2_gateway/Cargo.toml b/kych_oauth2_gateway/Cargo.toml @@ -2,6 +2,7 @@ name = "kych" version = "0.0.1" edition = "2024" +autobins = false [lib] name = "kych_oauth2_gateway_lib" @@ -12,10 +13,6 @@ name = "kych-oauth2-gateway" path = "src/main.rs" [[bin]] -name = "kych-oauth2-gateway-webhook-worker" -path = "src/bin/webhook_worker.rs" - -[[bin]] name = "kych-client-management" path = "src/bin/client_management_cli.rs" diff --git a/kych_oauth2_gateway/clients.conf.example b/kych_oauth2_gateway/clients.conf.example @@ -1,22 +0,0 @@ -# OAuth2 Gateway Clients Configuration -# -# Use the client management CLI to sync this file to the database: -# client-mgmt sync clients.conf - -[client_example] -client_id = 1 -client_secret = 2 -webhook_url = -verifier_url = -verifier_management_api_path = /management/api/verifications -redirect_uri = -accepted_issuer_dids = did:tdw:QmPEZPhDFR4nEYSFK5bMnvECqdpf1tPTPJuWs9QrMjCumw:identifier-reg.trust-infra.swiyu-int.admin.ch:api:v1:did:9a5559f0-b81c-4368-a170-e7b4ae424527 - -# [another_client] -# client_id = client_staging_01 -# client_secret = another_secret -# webhook_url = https://staging.example.com/webhook -# verifier_url = https://verifier-staging.example.com -# verifier_management_api_path = /api/v1/verifications -# redirect_uri = https://staging.example.com/callback -# accepted_issuer_dids = did:key:staging1 diff --git a/kych_oauth2_gateway/config.ini.example b/kych_oauth2_gateway/config.ini.example @@ -1,19 +0,0 @@ -[server] -host = -port = -#socket_path = - -[database] -url = - -[crypto] -nonce_bytes = 32 -token_bytes = 32 -authorization_code_bytes = 32 - -[webhook_worker] -retry_delay_server_error = 60 -retry_delay_forbidden = 60 -retry_delay_other = 3600 -fallback_poll_secs = 300 -batch_size = 100 diff --git a/kych_oauth2_gateway/src/bin/client_management_cli.rs b/kych_oauth2_gateway/src/bin/client_management_cli.rs @@ -4,11 +4,12 @@ //! //! Set DATABASE_URL environment variable to connect to the database. //! Usage: -//! client-mgmt client list -//! client-mgmt client show <client_id> -//! client-mgmt client create --client-id <id> --secret <secret> ... -//! client-mgmt client update <client_id> --webhook-url <url> -//! client-mgmt client delete <client_id> +//! kych-client-management list +//! kych-client-management show <client_id> +//! kych-client-management create --client-id <id> --secret <secret> ... +//! kych-client-management update <client_id> --webhook-url <url> +//! kych-client-management sync kych.conf +//! kych-client-management delete <client_id> use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; @@ -85,9 +86,9 @@ enum Commands { accepted_issuer_dids: Option<String>, }, - /// Sync clients from a configuration file + /// Sync clients from configuration file (reads [client_*] sections) Sync { - /// Path to clients.conf file + /// Path to kych.conf file config_file: String, /// Remove clients not in config file @@ -337,24 +338,24 @@ async fn cmd_sync_clients(pool: &sqlx::PgPool, config_file: &str, prune: bool) - for (section_name, properties) in ini.iter() { let section_name = match section_name { - Some(name) => name, - None => continue, + Some(name) if name.starts_with("client_") => name, + _ => continue, }; println!("\nProcessing section: [{}]", section_name); - let client_id = properties.get("client_id") - .ok_or_else(|| anyhow::anyhow!("Missing client_id in section [{}]", section_name))?; - let client_secret = properties.get("client_secret") - .ok_or_else(|| anyhow::anyhow!("Missing client_secret in section [{}]", section_name))?; - let webhook_url = properties.get("webhook_url") - .ok_or_else(|| anyhow::anyhow!("Missing webhook_url in section [{}]", section_name))?; - let verifier_url = properties.get("verifier_url") - .ok_or_else(|| anyhow::anyhow!("Missing verifier_url in section [{}]", section_name))?; - - let verifier_api_path = properties.get("verifier_management_api_path"); - let redirect_uri = properties.get("redirect_uri"); - let accepted_issuer_dids = properties.get("accepted_issuer_dids"); + let client_id = properties.get("CLIENT_ID") + .ok_or_else(|| anyhow::anyhow!("Missing CLIENT_ID in section [{}]", section_name))?; + let client_secret = properties.get("CLIENT_SECRET") + .ok_or_else(|| anyhow::anyhow!("Missing CLIENT_SECRET in section [{}]", section_name))?; + let webhook_url = properties.get("WEBHOOK_URL") + .ok_or_else(|| anyhow::anyhow!("Missing WEBHOOK_URL in section [{}]", section_name))?; + let verifier_url = properties.get("VERIFIER_URL") + .ok_or_else(|| anyhow::anyhow!("Missing VERIFIER_URL in section [{}]", section_name))?; + + let verifier_api_path = properties.get("VERIFIER_MANAGEMENT_API_PATH"); + let redirect_uri = properties.get("REDIRECT_URI"); + let accepted_issuer_dids = properties.get("ACCEPTED_ISSUER_DIDS"); synced_client_ids.insert(client_id.to_string()); diff --git a/kych_oauth2_gateway/src/config.rs b/kych_oauth2_gateway/src/config.rs @@ -3,12 +3,14 @@ use serde::{Deserialize, Serialize}; use ini::Ini; use std::path::Path; +const MAIN_SECTION: &str = "kych-oauth2-gateway"; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub server: ServerConfig, pub database: DatabaseConfig, pub crypto: CryptoConfig, - pub webhook_worker: WebhookWorkerConfig, + pub clients: Vec<ClientConfig>, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -16,6 +18,7 @@ pub struct ServerConfig { pub host: Option<String>, pub port: Option<u16>, pub socket_path: Option<String>, + pub socket_mode: u32, } impl ServerConfig { @@ -24,15 +27,15 @@ impl ServerConfig { let has_unix = self.socket_path.is_some(); if has_tcp && has_unix { - anyhow::bail!("Cannot specify both TCP (host/port) and Unix socket (socket_path)"); + anyhow::bail!("Cannot specify both TCP (HOST/PORT) and Unix socket (UNIXPATH)"); } if !has_tcp && !has_unix { - anyhow::bail!("Must specify either TCP (host/port) or Unix socket (socket_path)"); + anyhow::bail!("Must specify either TCP (HOST/PORT) or Unix socket (UNIXPATH)"); } if has_tcp && (self.host.is_none() || self.port.is_none()) { - anyhow::bail!("Host and port must be specified for TCP"); + anyhow::bail!("HOST and PORT must both be specified for TCP"); } Ok(()) @@ -56,12 +59,15 @@ pub struct CryptoConfig { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebhookWorkerConfig { - pub retry_delay_server_error: i64, - pub retry_delay_forbidden: i64, - pub retry_delay_other: i64, - pub fallback_poll_secs: u64, - pub batch_size: i64, +pub struct ClientConfig { + pub section_name: String, + pub client_id: String, + pub client_secret: String, + pub webhook_url: String, + pub verifier_url: String, + pub verifier_management_api_path: String, + pub redirect_uri: Option<String>, + pub accepted_issuer_dids: Option<String>, } impl Config { @@ -69,97 +75,110 @@ impl Config { let ini = Ini::load_from_file(path.as_ref()) .context("Failed to load config file")?; - let server_section = ini - .section(Some("server")) - .context("Missing [server] section")?; + let main_section = ini + .section(Some(MAIN_SECTION)) + .context(format!("Missing [{}] section", MAIN_SECTION))?; - let host = server_section.get("host").map(|s| s.to_string()); - let port = server_section - .get("port") + let host = main_section.get("HOST") + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()); + let port = main_section + .get("PORT") + .filter(|s| !s.is_empty()) .map(|s| s.parse::<u16>()) .transpose() - .context("Invalid port")?; - let socket_path = server_section.get("socket_path").map(|s| s.to_string()); + .context("Invalid PORT")?; + let socket_path = main_section.get("UNIXPATH") + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()); + let socket_mode = main_section + .get("UNIXPATH_MODE") + .filter(|s| !s.is_empty()) + .map(|s| u32::from_str_radix(s, 8)) + .transpose() + .context("Invalid UNIXPATH_MODE (expected octal, e.g. 666)")? + .unwrap_or(0o666); let server = ServerConfig { host, port, socket_path, + socket_mode, }; server.validate()?; - let database_section = ini - .section(Some("database")) - .context("Missing [database] section")?; - let database = DatabaseConfig { - url: database_section - .get("url") - .context("Missing database.url")? + url: main_section + .get("DATABASE") + .context("Missing DATABASE")? .to_string(), }; - let crypto_section = ini - .section(Some("crypto")) - .context("Missing [crypto] section")?; - let crypto = CryptoConfig { - nonce_bytes: crypto_section - .get("nonce_bytes") - .context("Missing crypto.nonce_bytes")? + nonce_bytes: main_section + .get("NONCE_BYTES") + .context("Missing NONCE_BYTES")? .parse() - .context("Invalid crypto.nonce_bytes")?, - token_bytes: crypto_section - .get("token_bytes") - .context("Missing crypto.token_bytes")? + .context("Invalid NONCE_BYTES")?, + token_bytes: main_section + .get("TOKEN_BYTES") + .context("Missing TOKEN_BYTES")? .parse() - .context("Invalid crypto.token_bytes")?, - authorization_code_bytes: crypto_section - .get("authorization_code_bytes") - .context("Missing crypto.authorization_code_bytes")? + .context("Invalid TOKEN_BYTES")?, + authorization_code_bytes: main_section + .get("AUTH_CODE_BYTES") + .context("Missing AUTH_CODE_BYTES")? .parse() - .context("Invalid crypto.authorization_code_bytes")?, + .context("Invalid AUTH_CODE_BYTES")?, }; - let webhook_worker_section = ini - .section(Some("webhook_worker")) - .context("Missing [webhook_worker] section")?; - - let webhook_worker = WebhookWorkerConfig { - retry_delay_server_error: webhook_worker_section - .get("retry_delay_server_error") - .context("Missing webhook_worker.retry_delay_server_error")? - .parse() - .context("Invalid webhook_worker.retry_delay_server_error")?, - retry_delay_forbidden: webhook_worker_section - .get("retry_delay_forbidden") - .context("Missing webhook_worker.retry_delay_forbidden")? - .parse() - .context("Invalid webhook_worker.retry_delay_forbidden")?, - retry_delay_other: webhook_worker_section - .get("retry_delay_other") - .context("Missing webhook_worker.retry_delay_other")? - .parse() - .context("Invalid webhook_worker.retry_delay_other")?, - fallback_poll_secs: webhook_worker_section - .get("fallback_poll_secs") - .context("Missing webhook_worker.fallback_poll_secs")? - .parse() - .context("Invalid webhook_worker.fallback_poll_secs")?, - batch_size: webhook_worker_section - .get("batch_size") - .context("Missing webhook_worker.batch_size")? - .parse() - .context("Invalid webhook_worker.batch_size")?, - }; + let mut clients = Vec::new(); + for (section_name, properties) in ini.iter() { + let section_name = match section_name { + Some(name) if name.starts_with("client_") => name, + _ => continue, + }; + + let client_id = properties.get("CLIENT_ID") + .context(format!("Missing CLIENT_ID in section [{}]", section_name))? + .to_string(); + let client_secret = properties.get("CLIENT_SECRET") + .context(format!("Missing CLIENT_SECRET in section [{}]", section_name))? + .to_string(); + let webhook_url = properties.get("WEBHOOK_URL") + .context(format!("Missing WEBHOOK_URL in section [{}]", section_name))? + .to_string(); + let verifier_url = properties.get("VERIFIER_URL") + .context(format!("Missing VERIFIER_URL in section [{}]", section_name))? + .to_string(); + let verifier_management_api_path = properties.get("VERIFIER_MANAGEMENT_API_PATH") + .unwrap_or("/management/api/verifications") + .to_string(); + let redirect_uri = properties.get("REDIRECT_URI") + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()); + let accepted_issuer_dids = properties.get("ACCEPTED_ISSUER_DIDS") + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()); + + clients.push(ClientConfig { + section_name: section_name.to_string(), + client_id, + client_secret, + webhook_url, + verifier_url, + verifier_management_api_path, + redirect_uri, + accepted_issuer_dids, + }); + } Ok(Config { server, database, crypto, - webhook_worker, + clients, }) } } - diff --git a/kych_oauth2_gateway/src/lib.rs b/kych_oauth2_gateway/src/lib.rs @@ -3,5 +3,4 @@ pub mod handlers; pub mod models; pub mod state; pub mod crypto; -pub mod db; -pub mod worker; -\ No newline at end of file +pub mod db; +\ No newline at end of file diff --git a/kych_oauth2_gateway/src/main.rs b/kych_oauth2_gateway/src/main.rs @@ -67,17 +67,17 @@ async fn main() -> Result<()> { if config.server.is_unix_socket() { let socket_path = config.server.socket_path.as_ref().unwrap(); + let socket_mode = config.server.socket_mode; if std::path::Path::new(socket_path).exists() { - tracing::warn!("Removing existing socket file: {}", socket_path); + tracing::warn!("Removing left-over `{}' from previous execution", socket_path); std::fs::remove_file(socket_path)?; } let listener = tokio::net::UnixListener::bind(socket_path)?; - let permissions = std::fs::Permissions::from_mode(0o766); - let _ = fs::set_permissions(socket_path, permissions); - - tracing::info!("Server listening on Unix socket: {}", socket_path); + let permissions = std::fs::Permissions::from_mode(socket_mode); + fs::set_permissions(socket_path, permissions)?; + tracing::info!("set socket '{}' to mode {:o}", socket_path, socket_mode); axum::serve(listener, app).await?; } else {