Skip to content

Commit 5f0f6ee

Browse files
committed
admin: Add encrypt-github-tokens command
1 parent 4c3cf36 commit 5f0f6ee

File tree

2 files changed

+111
-0
lines changed

2 files changed

+111
-0
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
}

src/bin/crates-admin/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod default_versions;
66
mod delete_crate;
77
mod delete_version;
88
mod dialoguer;
9+
mod encrypt_github_tokens;
910
mod enqueue_job;
1011
mod migrate;
1112
mod populate;
@@ -21,6 +22,7 @@ enum Command {
2122
BackfillOgImages(backfill_og_images::Opts),
2223
DeleteCrate(delete_crate::Opts),
2324
DeleteVersion(delete_version::Opts),
25+
EncryptGithubTokens(encrypt_github_tokens::Opts),
2426
Populate(populate::Opts),
2527
RenderReadmes(render_readmes::Opts),
2628
TransferCrates(transfer_crates::Opts),
@@ -51,6 +53,7 @@ async fn main() -> anyhow::Result<()> {
5153
Command::BackfillOgImages(opts) => backfill_og_images::run(opts).await,
5254
Command::DeleteCrate(opts) => delete_crate::run(opts).await,
5355
Command::DeleteVersion(opts) => delete_version::run(opts).await,
56+
Command::EncryptGithubTokens(opts) => encrypt_github_tokens::run(opts).await,
5457
Command::Populate(opts) => populate::run(opts).await,
5558
Command::RenderReadmes(opts) => render_readmes::run(opts).await,
5659
Command::TransferCrates(opts) => transfer_crates::run(opts).await,

0 commit comments

Comments
 (0)