kych

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

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 }