add: tool to delete all accounts that except allow listed
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
2012
Cargo.lock
generated
Normal file
2012
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "gitea-tools"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.99"
|
||||||
|
clap = { version = "4.5.47", features = ["derive"] }
|
||||||
|
dialoguer = "0.12.0"
|
||||||
|
env_logger = "0.11.8"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
log = "0.4.28"
|
||||||
|
reqwest = { version = "0.12.23", features=["json"]}
|
||||||
|
serde = { version = "1.0.220", features = ["derive"] }
|
||||||
|
serde_json = "1.0.144"
|
||||||
|
tokio = { version = "1.47.1", features = ["full"] }
|
||||||
|
tokio-task-pool = { version = "0.1.5", features = ["log"] }
|
||||||
72
src/lib.rs
Normal file
72
src/lib.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use reqwest::header::CONTENT_TYPE;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct GetTokenResp {
|
||||||
|
pub name: String,
|
||||||
|
pub sha1: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct Email {
|
||||||
|
pub username: String,
|
||||||
|
pub email: String,
|
||||||
|
pub verified: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_token(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
router: &Router,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
otp: Option<&str>,
|
||||||
|
) -> Result<GetTokenResp, anyhow::Error> {
|
||||||
|
let mut req = client
|
||||||
|
.post(router.create_token(username))
|
||||||
|
.basic_auth(username, Some(password))
|
||||||
|
.json(&json!({
|
||||||
|
"name": "gitea-tools",
|
||||||
|
"scopes": ["write:admin", "read:admin"],
|
||||||
|
}))
|
||||||
|
.header(CONTENT_TYPE, "application/json");
|
||||||
|
|
||||||
|
if let Some(otp) = otp {
|
||||||
|
req = req.header("X-Gitea-OTP", otp);
|
||||||
|
}
|
||||||
|
let resp = req.send().await?;
|
||||||
|
let resp = resp.json::<GetTokenResp>().await?;
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_emails(client: &reqwest::Client, router: &Router) -> Result<Vec<Email>, anyhow::Error> {
|
||||||
|
Ok(client.get(&router.list_emails()).send().await?.json::<Vec<Email>>().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn purge_user(client: &reqwest::Client, router: &Router, username: &str) -> Result<(), anyhow::Error> {
|
||||||
|
client.delete(router.delete_user(username) + "?purge=true").send().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Router {
|
||||||
|
base: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Router {
|
||||||
|
pub fn new(base: String) -> Self {
|
||||||
|
Self { base }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_token(&self, username: &str) -> String {
|
||||||
|
format!("{}/api/v1/users/{username}/tokens", self.base)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_emails(&self) -> String {
|
||||||
|
format!("{}/api/v1/admin/emails", self.base)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_user(&self, username: &str) -> String {
|
||||||
|
format!("{}/api/v1/admin/users/{username}", self.base)
|
||||||
|
}
|
||||||
|
}
|
||||||
100
src/main.rs
Normal file
100
src/main.rs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use clap::{Parser, arg};
|
||||||
|
use dialoguer::{Input, Password};
|
||||||
|
use gitea_tools::{Router, list_emails, purge_user};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use log::info;
|
||||||
|
use reqwest::{ClientBuilder, header};
|
||||||
|
use tokio_task_pool::Pool;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref ALLOW_LIST: HashSet<&'static str> = {
|
||||||
|
let mut m = HashSet::new();
|
||||||
|
m.insert("charles@tipsy.codes");
|
||||||
|
m.insert("gitea@local.domain");
|
||||||
|
m
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct Args {
|
||||||
|
#[arg(short, long)]
|
||||||
|
username: Option<String>,
|
||||||
|
#[arg(short, long)]
|
||||||
|
password: Option<String>,
|
||||||
|
#[arg(short, long)]
|
||||||
|
token: Option<String>,
|
||||||
|
#[arg(short, long)]
|
||||||
|
gitea_address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let router = Router::new(args.gitea_address);
|
||||||
|
|
||||||
|
let token: String;
|
||||||
|
if let Some(t) = args.token {
|
||||||
|
token = t;
|
||||||
|
} else {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let username: String;
|
||||||
|
if let Some(u) = args.username {
|
||||||
|
username = u;
|
||||||
|
} else {
|
||||||
|
username = Input::new()
|
||||||
|
.with_prompt("Gitea username")
|
||||||
|
.interact_text()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let password: String;
|
||||||
|
if let Some(p) = args.password {
|
||||||
|
password = p;
|
||||||
|
} else {
|
||||||
|
password = Password::new()
|
||||||
|
.with_prompt("Gitea password (no echo)")
|
||||||
|
.interact()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
token = ::gitea_tools::get_token(&client, &router, &username, &password, None)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.sha1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut headers = header::HeaderMap::new();
|
||||||
|
let mut auth_value = header::HeaderValue::from_str(&format!("token {token}")).unwrap();
|
||||||
|
auth_value.set_sensitive(true);
|
||||||
|
headers.insert(header::AUTHORIZATION, auth_value);
|
||||||
|
|
||||||
|
let client = ClientBuilder::new()
|
||||||
|
.default_headers(headers)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Get a list of active emails
|
||||||
|
let pool = Pool::bounded(10);
|
||||||
|
loop {
|
||||||
|
let emails = list_emails(&client, &router).await.unwrap();
|
||||||
|
if emails.len() == ALLOW_LIST.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for email in emails {
|
||||||
|
if ALLOW_LIST.contains(&email.email as &str) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let username = email.username;
|
||||||
|
let client = client.clone();
|
||||||
|
let router = router.clone();
|
||||||
|
pool.spawn(async move {
|
||||||
|
info!("Deleting {}", username);
|
||||||
|
purge_user(&client, &router, &username).await.unwrap();
|
||||||
|
info!("Deleted {}!", username);
|
||||||
|
}).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Do we need to wait for pool to finish?
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user