client_management_cli.rs (11398B)
1 //! OAuth2 Gateway CLI 2 //! 3 //! Command-line tool for managing OAuth2 Gateway clients. 4 //! 5 //! Usage (`--config` is optional; without it the standard search path applies, 6 //! ending at /etc/kych/kych.conf): 7 //! kych-client-management --config kych.conf list 8 //! kych-client-management --config kych.conf show <client_id> 9 //! kych-client-management --config kych.conf create --client-id <id> --secret <secret> ... 10 //! kych-client-management --config kych.conf update <client_id> --redirect-uri <url> 11 //! kych-client-management --config kych.conf sync 12 //! kych-client-management --config kych.conf delete <client_id> 13 14 use anyhow::{Context, Result}; 15 use clap::{Parser, Subcommand}; 16 use kych_oauth2_gateway_lib::{ 17 config::{CONFIG_SOURCE, Config}, 18 db, 19 }; 20 use std::collections::HashSet; 21 use taler_config::{CommonArgs, taler_main}; 22 23 #[derive(Parser, Debug)] 24 #[command(name = "kych-client-management")] 25 #[command(version)] 26 #[command(about = "manage OAuth 2.0 clients of the KyCH gateway")] 27 struct Args { 28 #[command(flatten)] 29 common: CommonArgs, 30 31 #[command(subcommand)] 32 command: Commands, 33 } 34 35 #[derive(Subcommand, Debug)] 36 enum Commands { 37 List, 38 39 Show { 40 client_id: String, 41 }, 42 43 Create { 44 /// Unique client identifier 45 #[arg(long)] 46 client_id: String, 47 48 /// Client secret (stored as hash) 49 #[arg(long)] 50 secret: String, 51 52 /// Swiyu verifier base URL 53 #[arg(long)] 54 verifier_url: String, 55 56 /// Verifier management API path (default: /management/api/verifications) 57 #[arg(long)] 58 verifier_api_path: Option<String>, 59 60 /// Default redirect URI for OAuth2 flow 61 #[arg(long)] 62 redirect_uri: String, 63 64 /// Accepted issuer DIDs in braces, e.g. {did1, did2} 65 #[arg(long)] 66 accepted_issuer_dids: Option<String>, 67 }, 68 69 Update { 70 client_id: String, 71 72 #[arg(long)] 73 verifier_url: Option<String>, 74 75 #[arg(long)] 76 verifier_api_path: Option<String>, 77 78 #[arg(long)] 79 redirect_uri: Option<String>, 80 81 #[arg(long)] 82 accepted_issuer_dids: Option<String>, 83 }, 84 85 /// Sync clients from configuration file (reads [client_*] sections) 86 Sync { 87 /// Remove clients not in config file 88 #[arg(long)] 89 prune: bool, 90 }, 91 92 /// Delete a client (WARNING: cascades to all sessions) 93 Delete { 94 client_id: String, 95 96 #[arg(long, short = 'y')] 97 yes: bool, 98 }, 99 } 100 101 fn main() { 102 let args = Args::parse(); 103 104 taler_main(CONFIG_SOURCE, args.common, async |cfg| { 105 run(Config::parse(cfg)?, args.command).await 106 }) 107 } 108 109 async fn run(config: Config, command: Commands) -> Result<()> { 110 let pool = db::create_pool(config.database.clone()) 111 .await 112 .context("Failed to connect to database")?; 113 114 match command { 115 Commands::List => cmd_list_clients(&pool).await?, 116 Commands::Show { client_id } => cmd_show_client(&pool, &client_id).await?, 117 Commands::Create { 118 client_id, 119 secret, 120 verifier_url, 121 verifier_api_path, 122 redirect_uri, 123 accepted_issuer_dids, 124 } => { 125 cmd_create_client( 126 &pool, 127 &client_id, 128 &secret, 129 &verifier_url, 130 verifier_api_path.as_deref(), 131 &redirect_uri, 132 accepted_issuer_dids.as_deref(), 133 ) 134 .await? 135 } 136 Commands::Update { 137 client_id, 138 verifier_url, 139 verifier_api_path, 140 redirect_uri, 141 accepted_issuer_dids, 142 } => { 143 cmd_update_client( 144 &pool, 145 &client_id, 146 verifier_url.as_deref(), 147 verifier_api_path.as_deref(), 148 redirect_uri.as_deref(), 149 accepted_issuer_dids.as_deref(), 150 ) 151 .await? 152 } 153 Commands::Sync { prune } => { 154 cmd_sync_clients(&pool, &config, prune).await? 155 } 156 Commands::Delete { client_id, yes } => { 157 cmd_delete_client(&pool, &client_id, yes).await? 158 } 159 } 160 161 Ok(()) 162 } 163 164 fn print_client_details(client: &db::clients::Client) { 165 use chrono::Local; 166 167 println!("{}", "=".repeat(60)); 168 println!("UUID: {}", client.id); 169 println!("Client ID: {}", client.client_id); 170 println!("Secret Hash: {}...", &client.secret_hash[..20.min(client.secret_hash.len())]); 171 println!("Verifier URL: {}", client.verifier_url); 172 println!("Verifier API Path: {}", client.verifier_management_api_path); 173 println!("Redirect URI(s): {}", client.redirect_uri); 174 println!("Accepted Issuer DIDs: {}", client.accepted_issuer_dids.as_deref().unwrap_or("(not set)")); 175 println!("Created: {}", client.created_at.with_timezone(&Local)); 176 println!("Updated: {}", client.updated_at.with_timezone(&Local)); 177 } 178 179 async fn cmd_list_clients(pool: &sqlx::PgPool) -> Result<()> { 180 let clients = db::clients::list_clients(pool).await?; 181 182 if clients.is_empty() { 183 println!("No clients registered."); 184 return Ok(()); 185 } 186 187 let total = clients.len(); 188 189 for (i, client) in clients.iter().enumerate() { 190 if i > 0 { 191 println!(); 192 } 193 print_client_details(&client); 194 } 195 196 println!(); 197 println!("{}", "=".repeat(60)); 198 println!("Total clients: {}", total); 199 200 Ok(()) 201 } 202 203 async fn cmd_show_client(pool: &sqlx::PgPool, client_id: &str) -> Result<()> { 204 let client = db::clients::get_client_by_id(pool, client_id) 205 .await? 206 .ok_or_else(|| anyhow::anyhow!("Client not found: {}", client_id))?; 207 208 print_client_details(&client); 209 210 Ok(()) 211 } 212 213 async fn cmd_create_client( 214 pool: &sqlx::PgPool, 215 client_id: &str, 216 secret: &str, 217 verifier_url: &str, 218 verifier_api_path: Option<&str>, 219 redirect_uri: &str, 220 accepted_issuer_dids: Option<&str>, 221 ) -> Result<()> { 222 let client = db::clients::register_client( 223 pool, 224 client_id, 225 secret, 226 verifier_url, 227 verifier_api_path, 228 redirect_uri, 229 accepted_issuer_dids, 230 ) 231 .await 232 .context("Failed to create client")?; 233 234 println!("Client created successfully."); 235 println!(); 236 print_client_details(&client); 237 238 Ok(()) 239 } 240 241 async fn cmd_update_client( 242 pool: &sqlx::PgPool, 243 client_id: &str, 244 verifier_url: Option<&str>, 245 verifier_api_path: Option<&str>, 246 redirect_uri: Option<&str>, 247 accepted_issuer_dids: Option<&str>, 248 ) -> Result<()> { 249 if verifier_url.is_none() && verifier_api_path.is_none() 250 && redirect_uri.is_none() && accepted_issuer_dids.is_none() { 251 anyhow::bail!("No fields to update. Specify at least one of: --verifier-url, --verifier-api-path, --redirect-uri, --accepted-issuer-dids"); 252 } 253 254 let client = db::clients::get_client_by_id(pool, client_id) 255 .await? 256 .ok_or_else(|| anyhow::anyhow!("Client not found: {}", client_id))?; 257 258 let updated = db::clients::update_client( 259 pool, 260 client.id, 261 verifier_url, 262 verifier_api_path, 263 redirect_uri, 264 accepted_issuer_dids, 265 ) 266 .await 267 .context("Failed to update client")?; 268 269 println!("Client updated successfully."); 270 println!(); 271 print_client_details(&updated); 272 273 Ok(()) 274 } 275 276 async fn cmd_delete_client(pool: &sqlx::PgPool, client_id: &str, skip_confirm: bool) -> Result<()> { 277 let client = db::clients::get_client_by_id(pool, client_id) 278 .await? 279 .ok_or_else(|| anyhow::anyhow!("Client not found: {}", client_id))?; 280 281 if !skip_confirm { 282 println!("WARNING: This will delete client '{}' and ALL associated data:", client_id); 283 println!(" - All sessions"); 284 println!(" - All tokens"); 285 println!(); 286 print!("Type 'yes' to confirm: "); 287 288 use std::io::{self, Write}; 289 io::stdout().flush()?; 290 291 let mut input = String::new(); 292 io::stdin().read_line(&mut input)?; 293 294 if input.trim() != "yes" { 295 println!("Aborted."); 296 return Ok(()); 297 } 298 } 299 300 let deleted = db::clients::delete_client(pool, client.id).await?; 301 302 if deleted { 303 println!("Client '{}' deleted successfully.", client_id); 304 } else { 305 println!("Client not found (may have been deleted already)."); 306 } 307 308 Ok(()) 309 } 310 311 async fn cmd_sync_clients(pool: &sqlx::PgPool, config: &Config, prune: bool) -> Result<()> { 312 println!("Syncing {} client(s) from configuration...", config.clients.len()); 313 314 let mut synced_client_ids = HashSet::new(); 315 let mut created_count = 0; 316 let mut updated_count = 0; 317 318 for client_config in &config.clients { 319 println!("\nProcessing section: [{}]", client_config.section_name); 320 321 synced_client_ids.insert(client_config.client_id.clone()); 322 323 let existing_client = db::clients::get_client_by_id(pool, &client_config.client_id).await?; 324 325 match existing_client { 326 Some(existing) => { 327 println!(" Client '{}' already exists, updating...", client_config.client_id); 328 db::clients::update_client( 329 pool, 330 existing.id, 331 Some(&client_config.verifier_url), 332 Some(&client_config.verifier_management_api_path), 333 Some(&client_config.redirect_uri), 334 client_config.accepted_issuer_dids.as_deref(), 335 ) 336 .await 337 .context(format!("Failed to update client '{}'", client_config.client_id))?; 338 updated_count += 1; 339 println!(" Updated client '{}'", client_config.client_id); 340 } 341 None => { 342 println!(" Creating new client '{}'...", client_config.client_id); 343 db::clients::register_client( 344 pool, 345 &client_config.client_id, 346 &client_config.client_secret, 347 &client_config.verifier_url, 348 Some(&client_config.verifier_management_api_path), 349 &client_config.redirect_uri, 350 client_config.accepted_issuer_dids.as_deref(), 351 ) 352 .await 353 .context(format!("Failed to create client '{}'", client_config.client_id))?; 354 created_count += 1; 355 println!(" Created client '{}'", client_config.client_id); 356 } 357 } 358 } 359 360 if prune { 361 println!("\nPruning clients not in configuration file..."); 362 let all_clients = db::clients::list_clients(pool).await?; 363 let mut pruned_count = 0; 364 365 for client in all_clients { 366 if !synced_client_ids.contains(&client.client_id) { 367 println!(" Deleting client '{}'...", client.client_id); 368 db::clients::delete_client(pool, client.id).await?; 369 pruned_count += 1; 370 } 371 } 372 println!("Pruned {} client(s)", pruned_count); 373 } 374 375 println!("\nSync complete:"); 376 println!(" Created: {}", created_count); 377 println!(" Updated: {}", updated_count); 378 379 Ok(()) 380 }