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