kych

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

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 }