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
|
||||
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
|
||||
|
||||
- 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
|
||||
notification email fails.
|
||||
(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_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> {
|
||||
let api_token = std::fs::read_to_string(Path::new("api_token"))?
|
||||
.trim()
|
||||
|
@ -164,9 +171,9 @@ fn set_spam(
|
|||
|
||||
async fn apply_classification(
|
||||
forge: &Forgejo,
|
||||
mailer: &SmtpConfig,
|
||||
db: Arc<Mutex<Db>>,
|
||||
classifier: &mut Classifier,
|
||||
actually_ban: Arc<ActuallyBan>,
|
||||
ids: &[(UserId, bool)],
|
||||
overwrite: bool,
|
||||
) {
|
||||
|
@ -176,7 +183,7 @@ async fn apply_classification(
|
|||
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
|
||||
// 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
|
||||
.unwrap_or_else(|err| eprintln!("Failed to lock or notify user {login}: {err}"));
|
||||
}
|
||||
|
@ -198,7 +205,7 @@ struct AppState {
|
|||
db: Arc<Mutex<Db>>,
|
||||
classifier: Arc<Mutex<Classifier>>,
|
||||
forge: Arc<Forgejo>,
|
||||
mailer: Arc<SmtpConfig>,
|
||||
actually_ban: Arc<ActuallyBan>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -306,9 +313,9 @@ async fn post_classified(
|
|||
|
||||
apply_classification(
|
||||
&data.forge,
|
||||
&data.mailer,
|
||||
data.db.clone(),
|
||||
classifier,
|
||||
data.actually_ban.clone(),
|
||||
&updates,
|
||||
overwrite,
|
||||
)
|
||||
|
@ -382,8 +389,14 @@ async fn main() -> anyhow::Result<()> {
|
|||
eprintln!("Eval 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 mailer = Arc::new(SmtpConfig::from_env().await?);
|
||||
eprintln!("Load users and repos");
|
||||
let (db, classifier) = load_db(&forge).await?;
|
||||
let db = Arc::new(Mutex::new(db));
|
||||
|
@ -393,7 +406,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
db: db.clone(),
|
||||
classifier: classifier.clone(),
|
||||
forge: forge.clone(),
|
||||
mailer: mailer.clone(),
|
||||
actually_ban: actually_ban.clone(),
|
||||
});
|
||||
|
||||
let _ = {
|
||||
|
@ -405,13 +418,14 @@ async fn main() -> anyhow::Result<()> {
|
|||
let _ = {
|
||||
let forge = forge.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 forge = forge.clone();
|
||||
let mailer = mailer.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");
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::classifier::Classifier;
|
||||
use crate::data::UserId;
|
||||
use crate::db::{Db, IsSpam};
|
||||
use crate::email::SmtpConfig;
|
||||
use crate::email;
|
||||
use crate::scrape;
|
||||
use anyhow::anyhow;
|
||||
use forgejo_api::Forgejo;
|
||||
|
@ -9,6 +9,7 @@ use std::collections::HashMap;
|
|||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::ActuallyBan;
|
||||
use crate::FORGEJO_POLL_DELAY;
|
||||
use crate::GRACE_PERIOD;
|
||||
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
|
||||
|
||||
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
|
||||
.admin_delete_user(
|
||||
login,
|
||||
forgejo_api::structs::AdminDeleteUserQuery { purge: Some(true) },
|
||||
)
|
||||
.await?;
|
||||
eprintln!("Success");
|
||||
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 {
|
||||
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)
|
||||
} else {
|
||||
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(
|
||||
forge: &Forgejo,
|
||||
mailer: &SmtpConfig,
|
||||
db: Arc<Mutex<Db>>,
|
||||
actually_ban: Arc<ActuallyBan>,
|
||||
user_id: UserId,
|
||||
) -> anyhow::Result<()> {
|
||||
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 !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();
|
||||
db.is_spam.insert(
|
||||
user_id,
|
||||
|
@ -219,7 +243,16 @@ pub async fn try_lock_and_notify_user(
|
|||
}
|
||||
|
||||
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();
|
||||
db.is_spam.insert(
|
||||
user_id,
|
||||
|
@ -243,8 +276,8 @@ pub async fn try_lock_and_notify_user(
|
|||
|
||||
pub async fn lock_and_notify_users(
|
||||
forge: Arc<Forgejo>,
|
||||
mailer: Arc<SmtpConfig>,
|
||||
db: Arc<Mutex<Db>>,
|
||||
actually_ban: Arc<ActuallyBan>,
|
||||
) {
|
||||
let mut spammers = Vec::new();
|
||||
{
|
||||
|
@ -257,7 +290,7 @@ pub async fn lock_and_notify_users(
|
|||
}
|
||||
|
||||
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
|
||||
.unwrap_or_else(|err| eprintln!("Failed to lock or notify user {login}: {err}"));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue