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:
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