Perform destructive actions only when ACTUALLY_BAN_USERS=true
This commit is contained in:
parent
d7e6646226
commit
f8375ca188
3 changed files with 79 additions and 20 deletions
16
README.md
16
README.md
|
@ -13,10 +13,22 @@
|
||||||
locally in `db.json`, no concrete action is taken. (Ultimately we will want to
|
locally in `db.json`, no concrete action is taken. (Ultimately we will want to
|
||||||
lock/delete accounts, etc.)
|
lock/delete accounts, etc.)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Forgery reads the following environment variables:
|
||||||
|
- `ACTUALLY_BAN_USERS`: define it (e.g. to `true`) to actually lock user
|
||||||
|
accounts, send notification emails and eventually delete user accounts. If not
|
||||||
|
defined (the default), no actual action is taken, spammers are only listed in
|
||||||
|
the database. The variable should be set in production, but probably not for
|
||||||
|
testing.
|
||||||
|
|
||||||
|
Environment variables that are relevant when `ACTUALLY_BAN_USERS=true`:
|
||||||
|
- `SMTP_ADDRESS`: address of the SMTP relay used to send email notifications
|
||||||
|
- `SMTP_USERNAME`: SMTP username
|
||||||
|
- `SMTP_PASSWORD`: SMTP password
|
||||||
|
|
||||||
## Todos
|
## Todos
|
||||||
|
|
||||||
- gate the actual account lock/email/deletion behind an environment variable
|
|
||||||
for easy testing and to limit bad surprises
|
|
||||||
- discuss the current design choices for when locking the account/sending a
|
- discuss the current design choices for when locking the account/sending a
|
||||||
notification email fails.
|
notification email fails.
|
||||||
(Current behavior is to periodically retry, avoid deleting if the account
|
(Current behavior is to periodically retry, avoid deleting if the account
|
||||||
|
|
32
src/main.rs
32
src/main.rs
|
@ -42,6 +42,13 @@ const GRACE_PERIOD: Duration = Duration::from_secs(30 * 24 * 3600); // 30 days
|
||||||
const GUESS_SPAM_THRESHOLD: f32 = 0.8;
|
const GUESS_SPAM_THRESHOLD: f32 = 0.8;
|
||||||
const GUESS_LEGIT_THRESHOLD: f32 = 0.3;
|
const GUESS_LEGIT_THRESHOLD: f32 = 0.3;
|
||||||
|
|
||||||
|
// Whether we are actually banning users or are instead in "testing" mode where
|
||||||
|
// we don't do anything. (Defaults to "No".)
|
||||||
|
pub enum ActuallyBan {
|
||||||
|
Yes { smtp: SmtpConfig },
|
||||||
|
No,
|
||||||
|
}
|
||||||
|
|
||||||
fn forge() -> anyhow::Result<Forgejo> {
|
fn forge() -> anyhow::Result<Forgejo> {
|
||||||
let api_token = std::fs::read_to_string(Path::new("api_token"))?
|
let api_token = std::fs::read_to_string(Path::new("api_token"))?
|
||||||
.trim()
|
.trim()
|
||||||
|
@ -164,9 +171,9 @@ fn set_spam(
|
||||||
|
|
||||||
async fn apply_classification(
|
async fn apply_classification(
|
||||||
forge: &Forgejo,
|
forge: &Forgejo,
|
||||||
mailer: &SmtpConfig,
|
|
||||||
db: Arc<Mutex<Db>>,
|
db: Arc<Mutex<Db>>,
|
||||||
classifier: &mut Classifier,
|
classifier: &mut Classifier,
|
||||||
|
actually_ban: Arc<ActuallyBan>,
|
||||||
ids: &[(UserId, bool)],
|
ids: &[(UserId, bool)],
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) {
|
) {
|
||||||
|
@ -176,7 +183,7 @@ async fn apply_classification(
|
||||||
let login = db.lock().unwrap().users.get(&user).unwrap().login.clone();
|
let login = db.lock().unwrap().users.get(&user).unwrap().login.clone();
|
||||||
// It is ok for any of these calls to fail now: a worker will periodically retry
|
// It is ok for any of these calls to fail now: a worker will periodically retry
|
||||||
// TODO: signal the worker to wake up instead of performing a manual call here
|
// TODO: signal the worker to wake up instead of performing a manual call here
|
||||||
workers::try_lock_and_notify_user(forge, mailer, db.clone(), user)
|
workers::try_lock_and_notify_user(forge, db.clone(), actually_ban.clone(), user)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|err| eprintln!("Failed to lock or notify user {login}: {err}"));
|
.unwrap_or_else(|err| eprintln!("Failed to lock or notify user {login}: {err}"));
|
||||||
}
|
}
|
||||||
|
@ -198,7 +205,7 @@ struct AppState {
|
||||||
db: Arc<Mutex<Db>>,
|
db: Arc<Mutex<Db>>,
|
||||||
classifier: Arc<Mutex<Classifier>>,
|
classifier: Arc<Mutex<Classifier>>,
|
||||||
forge: Arc<Forgejo>,
|
forge: Arc<Forgejo>,
|
||||||
mailer: Arc<SmtpConfig>,
|
actually_ban: Arc<ActuallyBan>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -306,9 +313,9 @@ async fn post_classified(
|
||||||
|
|
||||||
apply_classification(
|
apply_classification(
|
||||||
&data.forge,
|
&data.forge,
|
||||||
&data.mailer,
|
|
||||||
data.db.clone(),
|
data.db.clone(),
|
||||||
classifier,
|
classifier,
|
||||||
|
data.actually_ban.clone(),
|
||||||
&updates,
|
&updates,
|
||||||
overwrite,
|
overwrite,
|
||||||
)
|
)
|
||||||
|
@ -382,8 +389,14 @@ async fn main() -> anyhow::Result<()> {
|
||||||
eprintln!("Eval templates");
|
eprintln!("Eval templates");
|
||||||
let _ = *TEMPLATES;
|
let _ = *TEMPLATES;
|
||||||
|
|
||||||
|
let actually_ban = Arc::new(match std::env::var("ACTUALLY_BAN_USERS") {
|
||||||
|
Ok(_) => ActuallyBan::Yes {
|
||||||
|
smtp: SmtpConfig::from_env().await?,
|
||||||
|
},
|
||||||
|
Err(_) => ActuallyBan::No,
|
||||||
|
});
|
||||||
|
|
||||||
let forge = Arc::new(forge()?);
|
let forge = Arc::new(forge()?);
|
||||||
let mailer = Arc::new(SmtpConfig::from_env().await?);
|
|
||||||
eprintln!("Load users and repos");
|
eprintln!("Load users and repos");
|
||||||
let (db, classifier) = load_db(&forge).await?;
|
let (db, classifier) = load_db(&forge).await?;
|
||||||
let db = Arc::new(Mutex::new(db));
|
let db = Arc::new(Mutex::new(db));
|
||||||
|
@ -393,7 +406,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
classifier: classifier.clone(),
|
classifier: classifier.clone(),
|
||||||
forge: forge.clone(),
|
forge: forge.clone(),
|
||||||
mailer: mailer.clone(),
|
actually_ban: actually_ban.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let _ = {
|
let _ = {
|
||||||
|
@ -405,13 +418,14 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let _ = {
|
let _ = {
|
||||||
let forge = forge.clone();
|
let forge = forge.clone();
|
||||||
let db = db.clone();
|
let db = db.clone();
|
||||||
tokio::spawn(async move { workers::purge_spammer_accounts(forge, db) })
|
let actually_ban = actually_ban.clone();
|
||||||
|
tokio::spawn(async move { workers::purge_spammer_accounts(forge, db, actually_ban) })
|
||||||
};
|
};
|
||||||
let _ = {
|
let _ = {
|
||||||
let forge = forge.clone();
|
let forge = forge.clone();
|
||||||
let mailer = mailer.clone();
|
|
||||||
let db = db.clone();
|
let db = db.clone();
|
||||||
tokio::spawn(async move { workers::lock_and_notify_users(forge, mailer, db) })
|
let actually_ban = actually_ban.clone();
|
||||||
|
tokio::spawn(async move { workers::lock_and_notify_users(forge, db, actually_ban) })
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("Listening on http://127.0.0.1:8080");
|
println!("Listening on http://127.0.0.1:8080");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::classifier::Classifier;
|
use crate::classifier::Classifier;
|
||||||
use crate::data::UserId;
|
use crate::data::UserId;
|
||||||
use crate::db::{Db, IsSpam};
|
use crate::db::{Db, IsSpam};
|
||||||
use crate::email::SmtpConfig;
|
use crate::email;
|
||||||
use crate::scrape;
|
use crate::scrape;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use forgejo_api::Forgejo;
|
use forgejo_api::Forgejo;
|
||||||
|
@ -9,6 +9,7 @@ use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use crate::ActuallyBan;
|
||||||
use crate::FORGEJO_POLL_DELAY;
|
use crate::FORGEJO_POLL_DELAY;
|
||||||
use crate::GRACE_PERIOD;
|
use crate::GRACE_PERIOD;
|
||||||
use crate::{GUESS_LEGIT_THRESHOLD, GUESS_SPAM_THRESHOLD};
|
use crate::{GUESS_LEGIT_THRESHOLD, GUESS_SPAM_THRESHOLD};
|
||||||
|
@ -82,17 +83,32 @@ pub async fn refresh_user_data(
|
||||||
|
|
||||||
// Worker to delete spam accounts after their grace period expired
|
// Worker to delete spam accounts after their grace period expired
|
||||||
|
|
||||||
async fn try_purge_account(forge: &Forgejo, login: &str) -> anyhow::Result<()> {
|
async fn try_purge_account(
|
||||||
|
forge: &Forgejo,
|
||||||
|
login: &str,
|
||||||
|
actually_ban: &ActuallyBan,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if let ActuallyBan::No = actually_ban {
|
||||||
|
eprintln!("[Simulating: delete account of user {login}]");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("Deleting account of user {login}");
|
||||||
forge
|
forge
|
||||||
.admin_delete_user(
|
.admin_delete_user(
|
||||||
login,
|
login,
|
||||||
forgejo_api::structs::AdminDeleteUserQuery { purge: Some(true) },
|
forgejo_api::structs::AdminDeleteUserQuery { purge: Some(true) },
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
eprintln!("Success");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn purge_spammer_accounts(forge: Arc<Forgejo>, db: Arc<Mutex<Db>>) {
|
pub async fn purge_spammer_accounts(
|
||||||
|
forge: Arc<Forgejo>,
|
||||||
|
db: Arc<Mutex<Db>>,
|
||||||
|
actually_ban: Arc<ActuallyBan>,
|
||||||
|
) {
|
||||||
loop {
|
loop {
|
||||||
let mut classified_users = Vec::new();
|
let mut classified_users = Vec::new();
|
||||||
{
|
{
|
||||||
|
@ -129,7 +145,7 @@ pub async fn purge_spammer_accounts(forge: Arc<Forgejo>, db: Arc<Mutex<Db>>) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = try_purge_account(&forge, &login).await {
|
if let Err(e) = try_purge_account(&forge, &login, &actually_ban).await {
|
||||||
eprintln!("Error while deleting spammer account {login}: {:?}", e)
|
eprintln!("Error while deleting spammer account {login}: {:?}", e)
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Deleted spammer account {login}");
|
eprintln!("Deleted spammer account {login}");
|
||||||
|
@ -185,8 +201,8 @@ pub async fn lock_user_account(forge: &Forgejo, username: &str) -> anyhow::Resul
|
||||||
|
|
||||||
pub async fn try_lock_and_notify_user(
|
pub async fn try_lock_and_notify_user(
|
||||||
forge: &Forgejo,
|
forge: &Forgejo,
|
||||||
mailer: &SmtpConfig,
|
|
||||||
db: Arc<Mutex<Db>>,
|
db: Arc<Mutex<Db>>,
|
||||||
|
actually_ban: Arc<ActuallyBan>,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let (login, email, is_spam) = {
|
let (login, email, is_spam) = {
|
||||||
|
@ -205,7 +221,15 @@ pub async fn try_lock_and_notify_user(
|
||||||
|
|
||||||
if let Some((classified_at, locked, notified)) = is_spam {
|
if let Some((classified_at, locked, notified)) = is_spam {
|
||||||
if !locked {
|
if !locked {
|
||||||
lock_user_account(forge, &login).await?;
|
match actually_ban.as_ref() {
|
||||||
|
ActuallyBan::Yes { .. } => {
|
||||||
|
eprintln!("Locking account of user {login}");
|
||||||
|
lock_user_account(forge, &login).await?;
|
||||||
|
eprintln!("Success");
|
||||||
|
}
|
||||||
|
ActuallyBan::No => eprintln!("[Simulating: lock account of user {login}]"),
|
||||||
|
}
|
||||||
|
|
||||||
let db = &mut db.lock().unwrap();
|
let db = &mut db.lock().unwrap();
|
||||||
db.is_spam.insert(
|
db.is_spam.insert(
|
||||||
user_id,
|
user_id,
|
||||||
|
@ -219,7 +243,16 @@ pub async fn try_lock_and_notify_user(
|
||||||
}
|
}
|
||||||
|
|
||||||
if !notified {
|
if !notified {
|
||||||
crate::email::send_locked_account_notice(mailer, &login, &email).await?;
|
match actually_ban.as_ref() {
|
||||||
|
ActuallyBan::Yes { smtp } => {
|
||||||
|
eprintln!("Sending notification email to user {login}");
|
||||||
|
email::send_locked_account_notice(&smtp, &login, &email).await?;
|
||||||
|
eprintln!("Success");
|
||||||
|
}
|
||||||
|
ActuallyBan::No => {
|
||||||
|
eprintln!("[Simulating: send notification email to user {login}]")
|
||||||
|
}
|
||||||
|
}
|
||||||
let db = &mut db.lock().unwrap();
|
let db = &mut db.lock().unwrap();
|
||||||
db.is_spam.insert(
|
db.is_spam.insert(
|
||||||
user_id,
|
user_id,
|
||||||
|
@ -243,8 +276,8 @@ pub async fn try_lock_and_notify_user(
|
||||||
|
|
||||||
pub async fn lock_and_notify_users(
|
pub async fn lock_and_notify_users(
|
||||||
forge: Arc<Forgejo>,
|
forge: Arc<Forgejo>,
|
||||||
mailer: Arc<SmtpConfig>,
|
|
||||||
db: Arc<Mutex<Db>>,
|
db: Arc<Mutex<Db>>,
|
||||||
|
actually_ban: Arc<ActuallyBan>,
|
||||||
) {
|
) {
|
||||||
let mut spammers = Vec::new();
|
let mut spammers = Vec::new();
|
||||||
{
|
{
|
||||||
|
@ -257,7 +290,7 @@ pub async fn lock_and_notify_users(
|
||||||
}
|
}
|
||||||
|
|
||||||
for (user_id, login) in spammers {
|
for (user_id, login) in spammers {
|
||||||
try_lock_and_notify_user(&forge, &mailer, db.clone(), user_id)
|
try_lock_and_notify_user(&forge, db.clone(), actually_ban.clone(), user_id)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|err| eprintln!("Failed to lock or notify user {login}: {err}"));
|
.unwrap_or_else(|err| eprintln!("Failed to lock or notify user {login}: {err}"));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue