|
| 1 | +use anyhow::{Context, Result}; |
| 2 | +use crates_io::util::gh_token_encryption::GitHubTokenEncryption; |
| 3 | +use crates_io::{db, models::User}; |
| 4 | +use crates_io_database::schema::users; |
| 5 | +use diesel::prelude::*; |
| 6 | +use diesel_async::RunQueryDsl; |
| 7 | +use indicatif::{ProgressBar, ProgressIterator, ProgressStyle}; |
| 8 | +use secrecy::ExposeSecret; |
| 9 | + |
| 10 | +#[derive(clap::Parser, Debug)] |
| 11 | +#[command( |
| 12 | + name = "encrypt-github-tokens", |
| 13 | + about = "Encrypt existing plaintext GitHub tokens in the database.", |
| 14 | + long_about = "Backfill operation to encrypt existing plaintext GitHub tokens using AES-256-GCM. \ |
| 15 | + This reads users with plaintext tokens but no encrypted tokens, encrypts them, and \ |
| 16 | + updates the database with the encrypted versions." |
| 17 | +)] |
| 18 | +pub struct Opts {} |
| 19 | + |
| 20 | +pub async fn run(_opts: Opts) -> Result<()> { |
| 21 | + println!("Starting GitHub token encryption backfill…"); |
| 22 | + |
| 23 | + // Load encryption configuration |
| 24 | + let encryption = GitHubTokenEncryption::from_environment() |
| 25 | + .context("Failed to load encryption configuration")?; |
| 26 | + |
| 27 | + // Get database connection |
| 28 | + let mut conn = db::oneoff_connection() |
| 29 | + .await |
| 30 | + .context("Failed to establish database connection")?; |
| 31 | + |
| 32 | + // Query users with no encrypted tokens |
| 33 | + let users_to_encrypt = users::table |
| 34 | + .filter(users::gh_encrypted_token.is_null()) |
| 35 | + .select(User::as_select()) |
| 36 | + .load(&mut conn) |
| 37 | + .await |
| 38 | + .context("Failed to query users with plaintext tokens")?; |
| 39 | + |
| 40 | + let total_users = users_to_encrypt.len(); |
| 41 | + if total_users == 0 { |
| 42 | + println!("Found no users that need token encryption. Exiting."); |
| 43 | + return Ok(()); |
| 44 | + } |
| 45 | + |
| 46 | + println!("Found {total_users} users with plaintext tokens to encrypt"); |
| 47 | + |
| 48 | + let pb = ProgressBar::new(total_users as u64); |
| 49 | + pb.set_style(ProgressStyle::with_template( |
| 50 | + "{bar:60} ({pos}/{len}, ETA {eta}) {msg}", |
| 51 | + )?); |
| 52 | + |
| 53 | + let mut encrypted_count = 0; |
| 54 | + let mut failed_count = 0; |
| 55 | + |
| 56 | + for user in users_to_encrypt.into_iter().progress_with(pb.clone()) { |
| 57 | + let user_id = user.id; |
| 58 | + let plaintext_token = user.gh_access_token.expose_secret(); |
| 59 | + |
| 60 | + let encrypted_token = match encryption.encrypt(plaintext_token) { |
| 61 | + Ok(encrypted_token) => encrypted_token, |
| 62 | + Err(e) => { |
| 63 | + pb.suspend(|| eprintln!("Failed to encrypt token for user {user_id}: {e}")); |
| 64 | + failed_count += 1; |
| 65 | + continue; |
| 66 | + } |
| 67 | + }; |
| 68 | + |
| 69 | + // Update the user with the encrypted token |
| 70 | + if let Err(e) = diesel::update(users::table.find(user_id)) |
| 71 | + .set(users::gh_encrypted_token.eq(Some(encrypted_token))) |
| 72 | + .execute(&mut conn) |
| 73 | + .await |
| 74 | + { |
| 75 | + pb.suspend(|| eprintln!("Failed to update user {user_id}: {e}")); |
| 76 | + failed_count += 1; |
| 77 | + continue; |
| 78 | + } |
| 79 | + |
| 80 | + encrypted_count += 1; |
| 81 | + } |
| 82 | + |
| 83 | + pb.finish_with_message("Backfill completed!"); |
| 84 | + println!("Successfully encrypted: {encrypted_count} tokens"); |
| 85 | + |
| 86 | + if failed_count > 0 { |
| 87 | + eprintln!( |
| 88 | + "WARNING: {failed_count} tokens failed to encrypt. Please review the errors above." |
| 89 | + ); |
| 90 | + std::process::exit(1); |
| 91 | + } |
| 92 | + |
| 93 | + // Verify the backfill by checking for any remaining unencrypted tokens |
| 94 | + let remaining_unencrypted = users::table |
| 95 | + .filter(users::gh_encrypted_token.is_null()) |
| 96 | + .count() |
| 97 | + .get_result::<i64>(&mut conn) |
| 98 | + .await |
| 99 | + .context("Failed to count remaining unencrypted tokens")?; |
| 100 | + |
| 101 | + if remaining_unencrypted > 0 { |
| 102 | + eprintln!("WARNING: {remaining_unencrypted} users still have unencrypted tokens"); |
| 103 | + std::process::exit(1); |
| 104 | + } |
| 105 | + |
| 106 | + println!("Verification successful: All non-empty tokens have been encrypted!"); |
| 107 | + Ok(()) |
| 108 | +} |
0 commit comments