From d0af8ed2384980dfe97177650f2d745975e002dc Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 28 Apr 2025 12:30:58 +0200 Subject: [PATCH 1/4] database: Add `NewUsedJti` data access object --- .../src/models/trustpub/mod.rs | 2 ++ .../src/models/trustpub/used_jti.rs | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 crates/crates_io_database/src/models/trustpub/used_jti.rs diff --git a/crates/crates_io_database/src/models/trustpub/mod.rs b/crates/crates_io_database/src/models/trustpub/mod.rs index 634f500f46d..fb8bc09b789 100644 --- a/crates/crates_io_database/src/models/trustpub/mod.rs +++ b/crates/crates_io_database/src/models/trustpub/mod.rs @@ -1,3 +1,5 @@ mod github_config; +mod used_jti; pub use self::github_config::{GitHubConfig, NewGitHubConfig}; +pub use self::used_jti::NewUsedJti; diff --git a/crates/crates_io_database/src/models/trustpub/used_jti.rs b/crates/crates_io_database/src/models/trustpub/used_jti.rs new file mode 100644 index 00000000000..eced690bf0b --- /dev/null +++ b/crates/crates_io_database/src/models/trustpub/used_jti.rs @@ -0,0 +1,24 @@ +use crate::schema::trustpub_used_jtis; +use chrono::{DateTime, Utc}; +use diesel::prelude::*; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; + +#[derive(Debug, Insertable)] +#[diesel(table_name = trustpub_used_jtis, check_for_backend(diesel::pg::Pg))] +pub struct NewUsedJti<'a> { + pub jti: &'a str, + pub expires_at: DateTime, +} + +impl<'a> NewUsedJti<'a> { + pub fn new(jti: &'a str, expires_at: DateTime) -> Self { + Self { jti, expires_at } + } + + pub async fn insert(&self, conn: &mut AsyncPgConnection) -> QueryResult { + diesel::insert_into(trustpub_used_jtis::table) + .values(self) + .execute(conn) + .await + } +} From 9d8433a5409c2c500a70bd17b312037d7e188663 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 28 Apr 2025 12:30:24 +0200 Subject: [PATCH 2/4] database: Add `NewToken` data access object --- .../src/models/trustpub/mod.rs | 2 ++ .../src/models/trustpub/token.rs | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 crates/crates_io_database/src/models/trustpub/token.rs diff --git a/crates/crates_io_database/src/models/trustpub/mod.rs b/crates/crates_io_database/src/models/trustpub/mod.rs index fb8bc09b789..6a2ad6357b4 100644 --- a/crates/crates_io_database/src/models/trustpub/mod.rs +++ b/crates/crates_io_database/src/models/trustpub/mod.rs @@ -1,5 +1,7 @@ mod github_config; +mod token; mod used_jti; pub use self::github_config::{GitHubConfig, NewGitHubConfig}; +pub use self::token::NewToken; pub use self::used_jti::NewUsedJti; diff --git a/crates/crates_io_database/src/models/trustpub/token.rs b/crates/crates_io_database/src/models/trustpub/token.rs new file mode 100644 index 00000000000..80e6fcf5c84 --- /dev/null +++ b/crates/crates_io_database/src/models/trustpub/token.rs @@ -0,0 +1,22 @@ +use crate::schema::trustpub_tokens; +use chrono::{DateTime, Utc}; +use diesel::prelude::*; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; + +#[derive(Debug, Insertable)] +#[diesel(table_name = trustpub_tokens, check_for_backend(diesel::pg::Pg))] +pub struct NewToken<'a> { + pub expires_at: DateTime, + pub hashed_token: &'a [u8], + pub crate_ids: &'a [i32], +} + +impl NewToken<'_> { + pub async fn insert(&self, conn: &mut AsyncPgConnection) -> QueryResult<()> { + self.insert_into(trustpub_tokens::table) + .execute(conn) + .await?; + + Ok(()) + } +} From 6214128b5379f6e555641a05bc0f9ee37abbe34e Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 21 May 2025 14:55:28 +0200 Subject: [PATCH 3/4] worker: Implement `trustpub::DeleteExpiredTokens` background job This job deletes expired tokens from the database. --- src/bin/crates-admin/enqueue_job.rs | 5 ++ src/worker/jobs/mod.rs | 1 + src/worker/jobs/trustpub/delete_tokens.rs | 70 +++++++++++++++++++++++ src/worker/jobs/trustpub/mod.rs | 3 + src/worker/mod.rs | 1 + 5 files changed, 80 insertions(+) create mode 100644 src/worker/jobs/trustpub/delete_tokens.rs create mode 100644 src/worker/jobs/trustpub/mod.rs diff --git a/src/bin/crates-admin/enqueue_job.rs b/src/bin/crates-admin/enqueue_job.rs index 192fd562275..d4fb49c359b 100644 --- a/src/bin/crates-admin/enqueue_job.rs +++ b/src/bin/crates-admin/enqueue_job.rs @@ -49,6 +49,7 @@ pub enum Command { name: String, }, SyncUpdatesFeed, + TrustpubCleanup, } pub async fn run(command: Command) -> Result<()> { @@ -161,6 +162,10 @@ pub async fn run(command: Command) -> Result<()> { Command::SyncUpdatesFeed => { jobs::rss::SyncUpdatesFeed.enqueue(&mut conn).await?; } + Command::TrustpubCleanup => { + let job = jobs::trustpub::DeleteExpiredTokens; + job.enqueue(&mut conn).await?; + } }; Ok(()) diff --git a/src/worker/jobs/mod.rs b/src/worker/jobs/mod.rs index f7cbd6434b8..af8a008a652 100644 --- a/src/worker/jobs/mod.rs +++ b/src/worker/jobs/mod.rs @@ -12,6 +12,7 @@ mod readmes; pub mod rss; mod send_publish_notifications; mod sync_admins; +pub mod trustpub; mod typosquat; mod update_default_version; diff --git a/src/worker/jobs/trustpub/delete_tokens.rs b/src/worker/jobs/trustpub/delete_tokens.rs new file mode 100644 index 00000000000..4b528a93e37 --- /dev/null +++ b/src/worker/jobs/trustpub/delete_tokens.rs @@ -0,0 +1,70 @@ +use crate::worker::Environment; +use crates_io_database::schema::trustpub_tokens; +use crates_io_worker::BackgroundJob; +use diesel::prelude::*; +use diesel_async::RunQueryDsl; +use std::sync::Arc; + +/// A background job that deletes expired temporary access +/// tokens from the database. +#[derive(Deserialize, Serialize)] +pub struct DeleteExpiredTokens; + +impl BackgroundJob for DeleteExpiredTokens { + const JOB_NAME: &'static str = "trustpub::delete_expired_tokens"; + + type Context = Arc; + + async fn run(&self, ctx: Self::Context) -> anyhow::Result<()> { + let mut conn = ctx.deadpool.get().await?; + + diesel::delete(trustpub_tokens::table) + .filter(trustpub_tokens::expires_at.lt(diesel::dsl::now)) + .execute(&mut conn) + .await?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::util::TestApp; + use chrono::{TimeDelta, Utc}; + use crates_io_database::models::trustpub::NewToken; + use insta::assert_compact_debug_snapshot; + + #[tokio::test(flavor = "multi_thread")] + async fn test_expiry() -> anyhow::Result<()> { + let (app, _client) = TestApp::full().empty().await; + let mut conn = app.db_conn().await; + + let token = NewToken { + expires_at: Utc::now() + TimeDelta::minutes(30), + hashed_token: &[0xC0, 0xFF, 0xEE], + crate_ids: &[1], + }; + token.insert(&mut conn).await?; + + let token = NewToken { + expires_at: Utc::now() - TimeDelta::minutes(5), + hashed_token: &[0xBA, 0xAD, 0xF0, 0x0D], + crate_ids: &[2], + }; + token.insert(&mut conn).await?; + + DeleteExpiredTokens.enqueue(&mut conn).await?; + app.run_pending_background_jobs().await; + + // Check that the expired token was deleted + let crate_ids: Vec>> = trustpub_tokens::table + .select(trustpub_tokens::crate_ids) + .load(&mut conn) + .await?; + + assert_compact_debug_snapshot!(crate_ids, @"[[Some(1)]]"); + + Ok(()) + } +} diff --git a/src/worker/jobs/trustpub/mod.rs b/src/worker/jobs/trustpub/mod.rs new file mode 100644 index 00000000000..797771ea145 --- /dev/null +++ b/src/worker/jobs/trustpub/mod.rs @@ -0,0 +1,3 @@ +mod delete_tokens; + +pub use delete_tokens::DeleteExpiredTokens; diff --git a/src/worker/mod.rs b/src/worker/mod.rs index 0838b8bff4f..5b53f3dbd4f 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -43,5 +43,6 @@ impl RunnerExt for Runner> { .register_job_type::() .register_job_type::() .register_job_type::() + .register_job_type::() } } From 829fcadf767bdab906d9d8d6b51e7ca0d3720222 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Wed, 21 May 2025 15:02:17 +0200 Subject: [PATCH 4/4] worker: Implement `trustpub::DeleteExpiredJtis` background job This job deletes expired tokens from the database. --- src/bin/crates-admin/enqueue_job.rs | 3 ++ src/worker/jobs/trustpub/delete_jtis.rs | 68 +++++++++++++++++++++++++ src/worker/jobs/trustpub/mod.rs | 2 + src/worker/mod.rs | 1 + 4 files changed, 74 insertions(+) create mode 100644 src/worker/jobs/trustpub/delete_jtis.rs diff --git a/src/bin/crates-admin/enqueue_job.rs b/src/bin/crates-admin/enqueue_job.rs index d4fb49c359b..1f85346ef2e 100644 --- a/src/bin/crates-admin/enqueue_job.rs +++ b/src/bin/crates-admin/enqueue_job.rs @@ -165,6 +165,9 @@ pub async fn run(command: Command) -> Result<()> { Command::TrustpubCleanup => { let job = jobs::trustpub::DeleteExpiredTokens; job.enqueue(&mut conn).await?; + + let job = jobs::trustpub::DeleteExpiredJtis; + job.enqueue(&mut conn).await?; } }; diff --git a/src/worker/jobs/trustpub/delete_jtis.rs b/src/worker/jobs/trustpub/delete_jtis.rs new file mode 100644 index 00000000000..6e058fa7b72 --- /dev/null +++ b/src/worker/jobs/trustpub/delete_jtis.rs @@ -0,0 +1,68 @@ +use crate::worker::Environment; +use crates_io_database::schema::trustpub_used_jtis; +use crates_io_worker::BackgroundJob; +use diesel::prelude::*; +use diesel_async::RunQueryDsl; +use std::sync::Arc; + +/// A background job that deletes expired JSON Web Token IDs (JTIs) +/// tokens from the database. +#[derive(Deserialize, Serialize)] +pub struct DeleteExpiredJtis; + +impl BackgroundJob for DeleteExpiredJtis { + const JOB_NAME: &'static str = "trustpub::delete_expired_jtis"; + + type Context = Arc; + + async fn run(&self, ctx: Self::Context) -> anyhow::Result<()> { + let mut conn = ctx.deadpool.get().await?; + + diesel::delete(trustpub_used_jtis::table) + .filter(trustpub_used_jtis::expires_at.lt(diesel::dsl::now)) + .execute(&mut conn) + .await?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::util::TestApp; + use chrono::{TimeDelta, Utc}; + use crates_io_database::models::trustpub::NewUsedJti; + use insta::assert_compact_debug_snapshot; + + #[tokio::test(flavor = "multi_thread")] + async fn test_expiry() -> anyhow::Result<()> { + let (app, _client) = TestApp::full().empty().await; + let mut conn = app.db_conn().await; + + let jti = NewUsedJti { + expires_at: Utc::now() + TimeDelta::minutes(30), + jti: "foo", + }; + jti.insert(&mut conn).await?; + + let jti = NewUsedJti { + expires_at: Utc::now() - TimeDelta::minutes(5), + jti: "bar", + }; + jti.insert(&mut conn).await?; + + DeleteExpiredJtis.enqueue(&mut conn).await?; + app.run_pending_background_jobs().await; + + // Check that the expired token was deleted + let known_jtis: Vec = trustpub_used_jtis::table + .select(trustpub_used_jtis::jti) + .load(&mut conn) + .await?; + + assert_compact_debug_snapshot!(known_jtis, @r#"["foo"]"#); + + Ok(()) + } +} diff --git a/src/worker/jobs/trustpub/mod.rs b/src/worker/jobs/trustpub/mod.rs index 797771ea145..04a130afa53 100644 --- a/src/worker/jobs/trustpub/mod.rs +++ b/src/worker/jobs/trustpub/mod.rs @@ -1,3 +1,5 @@ +mod delete_jtis; mod delete_tokens; +pub use delete_jtis::DeleteExpiredJtis; pub use delete_tokens::DeleteExpiredTokens; diff --git a/src/worker/mod.rs b/src/worker/mod.rs index 5b53f3dbd4f..f22bd29d0a6 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -43,6 +43,7 @@ impl RunnerExt for Runner> { .register_job_type::() .register_job_type::() .register_job_type::() + .register_job_type::() .register_job_type::() } }