kych

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

commit adff7ecd440e76bcc6b864ff6357141bb9799e7b
parent 0dbf8467b074c683a6ba7e28ac8e10965870ba68
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date:   Sun, 23 Nov 2025 18:35:17 +0100

oauth2_gateway: implement client cli management

Diffstat:
Moauth2_gateway/Cargo.toml | 7+++++++
Doauth2_gateway/src/bin/cli.rs | 306-------------------------------------------------------------------------------
Aoauth2_gateway/src/bin/client_management_cli.rs | 298+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 305 insertions(+), 306 deletions(-)

diff --git a/oauth2_gateway/Cargo.toml b/oauth2_gateway/Cargo.toml @@ -15,6 +15,10 @@ path = "src/main.rs" name = "webhook-worker" path = "src/bin/webhook_worker.rs" +[[bin]] +name = "client-mgmt" +path = "src/bin/client_management_cli.rs" + [dependencies] # Web framework axum = "0.8.6" @@ -45,6 +49,9 @@ tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } # Error handling anyhow = "1.0.100" +# Environment +dotenvy = "0.15" + # Cryptography rand = "0.8.5" base64 = "0.22.1" diff --git a/oauth2_gateway/src/bin/cli.rs b/oauth2_gateway/src/bin/cli.rs @@ -1,306 +0,0 @@ -//! OAuth2 Gateway CLI -//! -//! Command-line tool for managing OAuth2 Gateway clients. -//! -//! Usage: -//! oauth2gw-cli -c config.ini client list -//! oauth2gw-cli -c config.ini client show <client_id> -//! oauth2gw-cli -c config.ini client create --client-id <id> --secret <secret> ... -//! oauth2gw-cli -c config.ini client update <client_id> --webhook-url <url> -//! oauth2gw-cli -c config.ini client delete <client_id> - -use anyhow::{Context, Result}; -use clap::{Parser, Subcommand}; -use oauth2_gateway::{config::Config, db}; - -#[derive(Parser, Debug)] -#[command(name = "oauth2gw-cli")] -#[command(version)] -#[command(about = "OAuth2 Gateway administration CLI")] -struct Args { - /// Configuration file path - #[arg(short = 'c', long = "config", value_name = "FILE")] - config: String, - - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand, Debug)] -enum Commands { - /// Manage clients - Client { - #[command(subcommand)] - action: ClientAction, - }, -} - -#[derive(Subcommand, Debug)] -enum ClientAction { - /// List all registered clients - List, - - /// Show details for a specific client - Show { - /// Client ID to show - client_id: String, - }, - - /// Register a new client - Create { - /// Unique client identifier - #[arg(long)] - client_id: String, - - /// Client secret (stored as hash) - #[arg(long)] - secret: String, - - /// Webhook URL for notifications - #[arg(long)] - webhook_url: String, - - /// Swiyu verifier base URL - #[arg(long)] - verifier_url: String, - - /// Verifier management API path (default: /management/api/verifications) - #[arg(long)] - verifier_api_path: Option<String>, - }, - - /// Update an existing client - Update { - /// Client ID to update - client_id: String, - - /// New webhook URL - #[arg(long)] - webhook_url: Option<String>, - - /// New verifier URL - #[arg(long)] - verifier_url: Option<String>, - - /// New verifier management API path - #[arg(long)] - verifier_api_path: Option<String>, - }, - - /// Delete a client (WARNING: cascades to all sessions) - Delete { - /// Client ID to delete - client_id: String, - - /// Skip confirmation prompt - #[arg(long, short = 'y')] - yes: bool, - }, -} - -#[tokio::main] -async fn main() -> Result<()> { - let args = Args::parse(); - - let config = Config::from_file(&args.config) - .with_context(|| format!("Failed to load config from: {}", args.config))?; - - let pool = db::create_pool(&config.database.url) - .await - .context("Failed to connect to database")?; - - match args.command { - Commands::Client { action } => match action { - ClientAction::List => cmd_list_clients(&pool).await?, - ClientAction::Show { client_id } => cmd_show_client(&pool, &client_id).await?, - ClientAction::Create { - client_id, - secret, - webhook_url, - verifier_url, - verifier_api_path, - } => { - cmd_create_client( - &pool, - &client_id, - &secret, - &webhook_url, - &verifier_url, - verifier_api_path.as_deref(), - ) - .await? - } - ClientAction::Update { - client_id, - webhook_url, - verifier_url, - verifier_api_path, - } => { - cmd_update_client( - &pool, - &client_id, - webhook_url.as_deref(), - verifier_url.as_deref(), - verifier_api_path.as_deref(), - ) - .await? - } - ClientAction::Delete { client_id, yes } => { - cmd_delete_client(&pool, &client_id, yes).await? - } - }, - } - - Ok(()) -} - -async fn cmd_list_clients(pool: &sqlx::PgPool) -> Result<()> { - let clients = db::clients::list_clients(pool).await?; - - if clients.is_empty() { - println!("No clients registered."); - return Ok(()); - } - - println!("{:<36} {:<20} {:<40} {}", "UUID", "CLIENT_ID", "WEBHOOK_URL", "CREATED"); - println!("{}", "-".repeat(120)); - - for client in clients { - println!( - "{:<36} {:<20} {:<40} {}", - client.id, - truncate(&client.client_id, 20), - truncate(&client.webhook_url, 40), - client.created_at.format("%Y-%m-%d %H:%M") - ); - } - - Ok(()) -} - -async fn cmd_show_client(pool: &sqlx::PgPool, client_id: &str) -> Result<()> { - let client = db::clients::get_client_by_id(pool, client_id) - .await? - .ok_or_else(|| anyhow::anyhow!("Client not found: {}", client_id))?; - - println!("Client Details"); - println!("{}", "=".repeat(60)); - println!("UUID: {}", client.id); - println!("Client ID: {}", client.client_id); - println!("Secret Hash: {}...", &client.secret_hash[..20.min(client.secret_hash.len())]); - println!("Webhook URL: {}", client.webhook_url); - println!("Verifier URL: {}", client.verifier_url); - println!("Verifier API Path: {}", client.verifier_management_api_path); - println!("Created: {}", client.created_at); - println!("Updated: {}", client.updated_at); - - Ok(()) -} - -async fn cmd_create_client( - pool: &sqlx::PgPool, - client_id: &str, - secret: &str, - webhook_url: &str, - verifier_url: &str, - verifier_api_path: Option<&str>, -) -> Result<()> { - let client = db::clients::register_client( - pool, - client_id, - secret, - webhook_url, - verifier_url, - verifier_api_path, - ) - .await - .context("Failed to create client")?; - - println!("Client created successfully."); - println!(); - println!("UUID: {}", client.id); - println!("Client ID: {}", client.client_id); - println!("Webhook URL: {}", client.webhook_url); - - Ok(()) -} - -async fn cmd_update_client( - pool: &sqlx::PgPool, - client_id: &str, - webhook_url: Option<&str>, - verifier_url: Option<&str>, - verifier_api_path: Option<&str>, -) -> Result<()> { - if webhook_url.is_none() && verifier_url.is_none() && verifier_api_path.is_none() { - anyhow::bail!("No fields to update. Specify at least one of: --webhook-url, --verifier-url, --verifier-api-path"); - } - - let client = db::clients::get_client_by_id(pool, client_id) - .await? - .ok_or_else(|| anyhow::anyhow!("Client not found: {}", client_id))?; - - let updated = db::clients::update_client( - pool, - client.id, - webhook_url, - verifier_url, - verifier_api_path, - ) - .await - .context("Failed to update client")?; - - println!("Client updated successfully."); - println!(); - println!("UUID: {}", updated.id); - println!("Client ID: {}", updated.client_id); - println!("Webhook URL: {}", updated.webhook_url); - println!("Verifier URL: {}", updated.verifier_url); - println!("Verifier API Path: {}", updated.verifier_management_api_path); - - Ok(()) -} - -async fn cmd_delete_client(pool: &sqlx::PgPool, client_id: &str, skip_confirm: bool) -> Result<()> { - let client = db::clients::get_client_by_id(pool, client_id) - .await? - .ok_or_else(|| anyhow::anyhow!("Client not found: {}", client_id))?; - - if !skip_confirm { - println!("WARNING: This will delete client '{}' and ALL associated data:", client_id); - println!(" - All sessions"); - println!(" - All tokens"); - println!(" - All pending webhooks"); - println!(); - print!("Type 'yes' to confirm: "); - - use std::io::{self, Write}; - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - - if input.trim() != "yes" { - println!("Aborted."); - return Ok(()); - } - } - - let deleted = db::clients::delete_client(pool, client.id).await?; - - if deleted { - println!("Client '{}' deleted successfully.", client_id); - } else { - println!("Client not found (may have been deleted already)."); - } - - Ok(()) -} - -fn truncate(s: &str, max_len: usize) -> String { - if s.len() <= max_len { - s.to_string() - } else { - format!("{}...", &s[..max_len - 3]) - } -} diff --git a/oauth2_gateway/src/bin/client_management_cli.rs b/oauth2_gateway/src/bin/client_management_cli.rs @@ -0,0 +1,297 @@ +//! OAuth2 Gateway CLI +//! +//! Command-line tool for managing OAuth2 Gateway clients. +//! +//! 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> + +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; +use oauth2_gateway::db; +use std::env; + +#[derive(Parser, Debug)] +#[command(name = "client-mgmt")] + +#[command(version)] +#[command(about = "OAuth2 Gateway client management CLI")] +#[command(after_help = "Environment variables:\n DATABASE_URL PostgreSQL connection string (required)")] +struct Args { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + Client { + #[command(subcommand)] + action: ClientAction, + }, +} + +#[derive(Subcommand, Debug)] +enum ClientAction { + List, + + Show { + client_id: String, + }, + + Create { + /// Unique client identifier + #[arg(long)] + client_id: String, + + /// Client secret (stored as hash) + #[arg(long)] + secret: String, + + /// Webhook URL for notifications + #[arg(long)] + webhook_url: String, + + /// Swiyu verifier base URL + #[arg(long)] + verifier_url: String, + + /// Verifier management API path (default: /management/api/verifications) + #[arg(long)] + verifier_api_path: Option<String>, + }, + + Update { + client_id: String, + + #[arg(long)] + webhook_url: Option<String>, + + #[arg(long)] + verifier_url: Option<String>, + + #[arg(long)] + verifier_api_path: Option<String>, + }, + + /// Delete a client (WARNING: cascades to all sessions) + Delete { + client_id: String, + + #[arg(long, short = 'y')] + yes: bool, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + // Load .env (ignore if missing) + let _ = dotenvy::dotenv(); + + let args = Args::parse(); + + let database_url = env::var("DATABASE_URL") + .context("DATABASE_URL environment variable not set")?; + + let pool = db::create_pool(&database_url) + .await + .context("Failed to connect to database")?; + + match args.command { + Commands::Client { action } => match action { + ClientAction::List => cmd_list_clients(&pool).await?, + ClientAction::Show { client_id } => cmd_show_client(&pool, &client_id).await?, + ClientAction::Create { + client_id, + secret, + webhook_url, + verifier_url, + verifier_api_path, + } => { + cmd_create_client( + &pool, + &client_id, + &secret, + &webhook_url, + &verifier_url, + verifier_api_path.as_deref(), + ) + .await? + } + ClientAction::Update { + client_id, + webhook_url, + verifier_url, + verifier_api_path, + } => { + cmd_update_client( + &pool, + &client_id, + webhook_url.as_deref(), + verifier_url.as_deref(), + verifier_api_path.as_deref(), + ) + .await? + } + ClientAction::Delete { client_id, yes } => { + cmd_delete_client(&pool, &client_id, yes).await? + } + }, + } + + Ok(()) +} + +async fn cmd_list_clients(pool: &sqlx::PgPool) -> Result<()> { + let clients = db::clients::list_clients(pool).await?; + + if clients.is_empty() { + println!("No clients registered."); + return Ok(()); + } + + println!("{:<36} {:<20} {:<40} {}", "UUID", "CLIENT_ID", "WEBHOOK_URL", "CREATED"); + println!("{}", "-".repeat(120)); + + for client in clients { + println!( + "{:<36} {:<20} {:<40} {}", + client.id, + truncate(&client.client_id, 20), + truncate(&client.webhook_url, 40), + client.created_at.format("%Y-%m-%d %H:%M") + ); + } + + Ok(()) +} + +async fn cmd_show_client(pool: &sqlx::PgPool, client_id: &str) -> Result<()> { + let client = db::clients::get_client_by_id(pool, client_id) + .await? + .ok_or_else(|| anyhow::anyhow!("Client not found: {}", client_id))?; + + println!("Client Details"); + println!("{}", "=".repeat(60)); + println!("UUID: {}", client.id); + println!("Client ID: {}", client.client_id); + println!("Secret Hash: {}...", &client.secret_hash[..20.min(client.secret_hash.len())]); + println!("Webhook URL: {}", client.webhook_url); + println!("Verifier URL: {}", client.verifier_url); + println!("Verifier API Path: {}", client.verifier_management_api_path); + println!("Created: {}", client.created_at); + println!("Updated: {}", client.updated_at); + + Ok(()) +} + +async fn cmd_create_client( + pool: &sqlx::PgPool, + client_id: &str, + secret: &str, + webhook_url: &str, + verifier_url: &str, + verifier_api_path: Option<&str>, +) -> Result<()> { + let client = db::clients::register_client( + pool, + client_id, + secret, + webhook_url, + verifier_url, + verifier_api_path, + ) + .await + .context("Failed to create client")?; + + println!("Client created successfully."); + println!(); + println!("UUID: {}", client.id); + println!("Client ID: {}", client.client_id); + println!("Webhook URL: {}", client.webhook_url); + + Ok(()) +} + +async fn cmd_update_client( + pool: &sqlx::PgPool, + client_id: &str, + webhook_url: Option<&str>, + verifier_url: Option<&str>, + verifier_api_path: Option<&str>, +) -> Result<()> { + if webhook_url.is_none() && verifier_url.is_none() && verifier_api_path.is_none() { + anyhow::bail!("No fields to update. Specify at least one of: --webhook-url, --verifier-url, --verifier-api-path"); + } + + let client = db::clients::get_client_by_id(pool, client_id) + .await? + .ok_or_else(|| anyhow::anyhow!("Client not found: {}", client_id))?; + + let updated = db::clients::update_client( + pool, + client.id, + webhook_url, + verifier_url, + verifier_api_path, + ) + .await + .context("Failed to update client")?; + + println!("Client updated successfully."); + println!(); + println!("UUID: {}", updated.id); + println!("Client ID: {}", updated.client_id); + println!("Webhook URL: {}", updated.webhook_url); + println!("Verifier URL: {}", updated.verifier_url); + println!("Verifier API Path: {}", updated.verifier_management_api_path); + + Ok(()) +} + +async fn cmd_delete_client(pool: &sqlx::PgPool, client_id: &str, skip_confirm: bool) -> Result<()> { + let client = db::clients::get_client_by_id(pool, client_id) + .await? + .ok_or_else(|| anyhow::anyhow!("Client not found: {}", client_id))?; + + if !skip_confirm { + println!("WARNING: This will delete client '{}' and ALL associated data:", client_id); + println!(" - All sessions"); + println!(" - All tokens"); + println!(" - All pending webhooks"); + println!(); + print!("Type 'yes' to confirm: "); + + use std::io::{self, Write}; + io::stdout().flush()?; + + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + + if input.trim() != "yes" { + println!("Aborted."); + return Ok(()); + } + } + + let deleted = db::clients::delete_client(pool, client.id).await?; + + if deleted { + println!("Client '{}' deleted successfully.", client_id); + } else { + println!("Client not found (may have been deleted already)."); + } + + Ok(()) +} + +fn truncate(s: &str, max_len: usize) -> String { + if s.len() <= max_len { + s.to_string() + } else { + format!("{}...", &s[..max_len - 3]) + } +} +\ No newline at end of file