From 1e50d693343bcfaf2f4f99411622a1c4c0ceff2c Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 23 Dec 2024 14:33:19 +0100 Subject: [PATCH] controllers/krate/delete: Send deletion notification email after successful deletion --- src/controllers/krate/delete.rs | 52 +++++++++++++++++++ ...delete__tests__happy_path_new_crate-2.snap | 33 ++++++++++++ ...delete__tests__happy_path_old_crate-2.snap | 33 ++++++++++++ ..._tests__happy_path_really_old_crate-2.snap | 33 ++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 src/controllers/krate/snapshots/crates_io__controllers__krate__delete__tests__happy_path_new_crate-2.snap create mode 100644 src/controllers/krate/snapshots/crates_io__controllers__krate__delete__tests__happy_path_old_crate-2.snap create mode 100644 src/controllers/krate/snapshots/crates_io__controllers__krate__delete__tests__happy_path_really_old_crate-2.snap diff --git a/src/controllers/krate/delete.rs b/src/controllers/krate/delete.rs index 994f8d4a14..052171889d 100644 --- a/src/controllers/krate/delete.rs +++ b/src/controllers/krate/delete.rs @@ -1,6 +1,7 @@ use crate::app::AppState; use crate::auth::AuthCheck; use crate::controllers::krate::CratePath; +use crate::email::Email; use crate::models::{NewDeletedCrate, Rights}; use crate::schema::{crate_downloads, crates, dependencies}; use crate::util::errors::{custom, AppResult, BoxedAppError}; @@ -79,6 +80,7 @@ pub async fn delete_crate(path: CratePath, parts: Parts, app: AppState) -> AppRe } } + let crate_name = krate.name.clone(); conn.transaction(|conn| { async move { diesel::delete(crates::table.find(krate.id)) @@ -116,6 +118,23 @@ pub async fn delete_crate(path: CratePath, parts: Parts, app: AppState) -> AppRe }) .await?; + let email_future = async { + if let Some(recipient) = user.email(&mut conn).await? { + let email = CrateDeletionEmail { + user: &user.gh_login, + krate: &crate_name, + }; + + app.emails.send(&recipient, email).await? + } + + Ok::<_, anyhow::Error>(()) + }; + + if let Err(err) = email_future.await { + error!("Failed to send crate deletion email: {err}"); + } + Ok(StatusCode::NO_CONTENT) } @@ -147,6 +166,33 @@ async fn has_rev_dep(conn: &mut AsyncPgConnection, crate_id: i32) -> QueryResult Ok(rev_dep.is_some()) } +/// Email template for notifying a crate owner about a crate being deleted. +/// +/// The owner usually should be aware of the deletion since they initiated it, +/// but this email can be helpful in detecting malicious account activity. +#[derive(Debug, Clone)] +struct CrateDeletionEmail<'a> { + user: &'a str, + krate: &'a str, +} + +impl Email for CrateDeletionEmail<'_> { + fn subject(&self) -> String { + format!("crates.io: Deleted \"{}\" crate", self.krate) + } + + fn body(&self) -> String { + format!( + "Hi {}, + +this is a confirmation email for the deletion of your \"{}\" crate. + +If you did not initiate this deletion, your account may have been compromised. Please contact us at help@crates.io.", + self.user, self.krate + ) + } +} + #[cfg(test)] mod tests { use super::*; @@ -185,6 +231,8 @@ mod tests { assert_eq!(response.status(), StatusCode::NO_CONTENT); assert!(response.body().is_empty()); + assert_snapshot!(app.emails_snapshot().await); + // Assert that the crate no longer exists assert_crate_exists(&anon, "foo", false).await; assert!(!upstream.crate_exists("foo")?); @@ -220,6 +268,8 @@ mod tests { assert_eq!(response.status(), StatusCode::NO_CONTENT); assert!(response.body().is_empty()); + assert_snapshot!(app.emails_snapshot().await); + // Assert that the crate no longer exists assert_crate_exists(&anon, "foo", false).await; assert!(!upstream.crate_exists("foo")?); @@ -255,6 +305,8 @@ mod tests { assert_eq!(response.status(), StatusCode::NO_CONTENT); assert!(response.body().is_empty()); + assert_snapshot!(app.emails_snapshot().await); + // Assert that the crate no longer exists assert_crate_exists(&anon, "foo", false).await; assert!(!upstream.crate_exists("foo")?); diff --git a/src/controllers/krate/snapshots/crates_io__controllers__krate__delete__tests__happy_path_new_crate-2.snap b/src/controllers/krate/snapshots/crates_io__controllers__krate__delete__tests__happy_path_new_crate-2.snap new file mode 100644 index 0000000000..2eb8dada07 --- /dev/null +++ b/src/controllers/krate/snapshots/crates_io__controllers__krate__delete__tests__happy_path_new_crate-2.snap @@ -0,0 +1,33 @@ +--- +source: src/controllers/krate/delete.rs +expression: app.emails_snapshot().await +snapshot_kind: text +--- +To: foo@example.com +From: crates.io +Subject: crates.io: Successfully published foo@1.0.0 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +Hello foo! + +A new version of the package foo (1.0.0) was published by your account (htt= +ps://crates.io/users/foo) at [0000-00-00T00:00:00Z]. + +If you have questions or security concerns, you can contact us at help@crat= +es.io. If you would like to stop receiving these security notifications, yo= +u can disable them in your account settings. +---------------------------------------- + +To: foo@example.com +From: crates.io +Subject: crates.io: Deleted "foo" crate +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +Hi foo, + +this is a confirmation email for the deletion of your "foo" crate. + +If you did not initiate this deletion, your account may have been compromis= +ed. Please contact us at help@crates.io. diff --git a/src/controllers/krate/snapshots/crates_io__controllers__krate__delete__tests__happy_path_old_crate-2.snap b/src/controllers/krate/snapshots/crates_io__controllers__krate__delete__tests__happy_path_old_crate-2.snap new file mode 100644 index 0000000000..2eb8dada07 --- /dev/null +++ b/src/controllers/krate/snapshots/crates_io__controllers__krate__delete__tests__happy_path_old_crate-2.snap @@ -0,0 +1,33 @@ +--- +source: src/controllers/krate/delete.rs +expression: app.emails_snapshot().await +snapshot_kind: text +--- +To: foo@example.com +From: crates.io +Subject: crates.io: Successfully published foo@1.0.0 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +Hello foo! + +A new version of the package foo (1.0.0) was published by your account (htt= +ps://crates.io/users/foo) at [0000-00-00T00:00:00Z]. + +If you have questions or security concerns, you can contact us at help@crat= +es.io. If you would like to stop receiving these security notifications, yo= +u can disable them in your account settings. +---------------------------------------- + +To: foo@example.com +From: crates.io +Subject: crates.io: Deleted "foo" crate +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +Hi foo, + +this is a confirmation email for the deletion of your "foo" crate. + +If you did not initiate this deletion, your account may have been compromis= +ed. Please contact us at help@crates.io. diff --git a/src/controllers/krate/snapshots/crates_io__controllers__krate__delete__tests__happy_path_really_old_crate-2.snap b/src/controllers/krate/snapshots/crates_io__controllers__krate__delete__tests__happy_path_really_old_crate-2.snap new file mode 100644 index 0000000000..2eb8dada07 --- /dev/null +++ b/src/controllers/krate/snapshots/crates_io__controllers__krate__delete__tests__happy_path_really_old_crate-2.snap @@ -0,0 +1,33 @@ +--- +source: src/controllers/krate/delete.rs +expression: app.emails_snapshot().await +snapshot_kind: text +--- +To: foo@example.com +From: crates.io +Subject: crates.io: Successfully published foo@1.0.0 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +Hello foo! + +A new version of the package foo (1.0.0) was published by your account (htt= +ps://crates.io/users/foo) at [0000-00-00T00:00:00Z]. + +If you have questions or security concerns, you can contact us at help@crat= +es.io. If you would like to stop receiving these security notifications, yo= +u can disable them in your account settings. +---------------------------------------- + +To: foo@example.com +From: crates.io +Subject: crates.io: Deleted "foo" crate +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +Hi foo, + +this is a confirmation email for the deletion of your "foo" crate. + +If you did not initiate this deletion, your account may have been compromis= +ed. Please contact us at help@crates.io.