use crate::{get_now_iso, random_string, AccountWolves, Accounts, AccountsNew, Config, State}; use ldap3::{exop::PasswordModify, LdapConn, Scope}; use lettre::{ message::{header, MultiPart, SinglePart}, transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport, }; use maud::html; use sqlx::{Error, Pool, Sqlite}; use std::collections::HashSet; use tide::{ prelude::{json, Deserialize}, Request, }; pub mod email { use super::*; #[derive(Debug, Deserialize)] struct SignupEmail { email: String, } pub async fn submit(mut req: Request) -> tide::Result { let SignupEmail { email, } = req.body_json().await?; let config = &req.state().config; let db = &req.state().db; for record in get_wolves_mail(db, &email).await { // skynet emails not permitted if record.email.trim().ends_with("@skynet.ie") { continue; } // check if the email is already in the db if !check(db, &record.email).await { continue; } // generate a auth key let auth = random_string(75); match send_mail(config, &record, &auth) { Ok(_) => match save_to_db(db, &record, &auth).await { Ok(_) => {} Err(e) => { println!("Unable to save to db {} {e:?}", &record.email); } }, Err(e) => { println!("Unable to send mail to {} {e:?}", &record.email); } } } Ok(json!({"result": "success"}).into()) } pub async fn get_wolves_mail(db: &Pool, mail: &str) -> Vec { sqlx::query_as::<_, AccountWolves>( r#" SELECT * FROM accounts_wolves WHERE email = ? "#, ) .bind(mail) .fetch_all(db) .await .unwrap_or(vec![]) } async fn check(db: &Pool, mail: &str) -> bool { check_pending(db, mail).await && check_users(db, mail).await } async fn check_users(db: &Pool, mail: &str) -> bool { sqlx::query_as::<_, Accounts>( r#" SELECT * FROM accounts WHERE mail == ? "#, ) .bind(mail) .fetch_all(db) .await .unwrap_or(vec![]) .is_empty() } async fn check_pending(db: &Pool, mail: &str) -> bool { sqlx::query_as::<_, AccountsNew>( r#" SELECT * FROM accounts_new WHERE mail == ? "#, ) .bind(mail) .fetch_all(db) .await .unwrap_or(vec![]) .is_empty() } // using https://github.com/lettre/lettre/blob/57886c367d69b4d66300b322c94bd910b1eca364/examples/maud_html.rs fn send_mail(config: &Config, record: &AccountWolves, auth: &str) -> Result { let recipient = &record.name_first; let mail = &record.email; let url_base = "https://account.skynet.ie"; let link_new = format!("{url_base}/register?auth={auth}"); let link_mod = format!("{url_base}/modify"); let discord = "https://discord.skynet.ie"; let sender = format!("UL Computer Society <{}>", &config.mail_user); // Create the html we want to send. let html = html! { head { title { "Hello from Skynet!" } style type="text/css" { "h2, h4 { font-family: Arial, Helvetica, sans-serif; }" } } div style="display: flex; flex-direction: column; align-items: center;" { h2 { "Hello from Skynet!" } // Substitute in the name of our recipient. p { "Hi " (recipient) "," } p { "As part of the UL Computer Society you get an account on our Skynet cluster." br; "This gives you access to some of teh various services we offer:" ul { li { "Email" } li { "Gitlab" } li { "Linux Webhost" } } br; "The following invite will remain active until the end of year." } p { "If you are a new member please use the following link:" br; a href=(link_new) { (link_new) } } p { "If you are a returning user please set an email for your account at:" br; a href=(link_mod) { (link_mod) } } p { "If you have issues please refer to our Discord server:" br; a href=(discord) { (discord) } } p { "Skynet Team" br; "UL Computer Society" } } }; let body_text = format!( r#" Hi {recipient} As part of the UL Computer Society you get an account on our Skynet cluster. This gives you access to some of teh various services we offer: * Email * Gitlab * Linux Webhost The following invite will remain active until the end of year. If you are a new member please use the following link: {link_new} If you are a returning user please set an email for your account at: {link_mod} If you have issues please refer to our Discord server: {discord} Skynet Team UL Computer Society "# ); // Build the message. let email = Message::builder() .from(sender.parse().unwrap()) .to(mail.parse().unwrap()) .subject("Skynet: New Account.") .multipart( // This is composed of two parts. // also helps not trip spam settings (uneven number of url's MultiPart::alternative() .singlepart(SinglePart::builder().header(header::ContentType::TEXT_PLAIN).body(body_text)) .singlepart(SinglePart::builder().header(header::ContentType::TEXT_HTML).body(html.into_string())), ) .expect("failed to build email"); let creds = Credentials::new(config.mail_user.clone(), config.mail_pass.clone()); // Open a remote connection to gmail using STARTTLS let mailer = SmtpTransport::starttls_relay(&config.mail_smtp).unwrap().credentials(creds).build(); // Send the email mailer.send(&email) } async fn save_to_db(db: &Pool, record: &AccountWolves, auth: &str) -> Result, sqlx::Error> { sqlx::query_as::<_, AccountsNew>( " INSERT OR REPLACE INTO accounts_new (mail, auth_code, date_iso, date_expiry, name_first, name_surname, id_student) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ", ) .bind(record.email.to_owned()) .bind(auth.to_owned()) .bind(get_now_iso(false)) .bind(record.expiry.to_owned()) .bind(record.name_first.to_owned()) .bind(record.name_second.to_owned()) .bind(record.id_student.to_owned()) .fetch_optional(db) .await } } pub mod account { use super::*; use crate::update_group; #[derive(Debug, Deserialize)] struct LdapNewUser { auth: String, user: String, pass: String, } /// Handles initial detail entering page /// Verify users have access to said email /// Get users to set username and password. pub async fn submit(mut req: Request) -> tide::Result { let LdapNewUser { auth, user, pass, } = req.body_json().await?; let config = &req.state().config; let db = &req.state().db; // ensure there are no old requests db_pending_clear_expired(db).await?; let user_db = if let Some(x) = db_get_user(db, &auth).await { x } else { return Ok(json!({"result": "error", "error": "Invalid auth"}).into()); }; if let Some(error) = is_valid_name(&user) { return Ok(json!({"result": "error", "error": error}).into()); } // easier to give each request its own connection let mut ldap = LdapConn::new(&config.ldap_host)?; // ldap3 docs say a blank username and pass is an anon bind ldap.simple_bind("", "")?.success()?; let filter_dn = format!("(uid={})", &user); if let Ok(x) = ldap.search("ou=users,dc=skynet,dc=ie", Scope::OneLevel, &filter_dn, vec!["*"]) { if let Ok((rs, _res)) = x.success() { if !rs.is_empty() { return Ok(json!({"result": "error", "error": "username not available"}).into()); } } } // done with anon ldap ldap.unbind()?; ldap_create_account(config, db, user_db, &user, &pass).await?; // account now created, delete from the new table account_verification_clear_pending(db, &auth).await?; Ok(json!({"result": "success"}).into()) } // clear the db of expired ones before checking for username and validating inputs async fn db_pending_clear_expired(pool: &Pool) -> Result, Error> { sqlx::query_as::<_, AccountsNew>( r#" DELETE FROM accounts_new WHERE date_expiry < ? "#, ) .bind(get_now_iso(true)) .fetch_all(pool) .await } fn is_valid_name(name: &str) -> Option { // max length is 31 chars if name.len() >= 32 { return Some(String::from("Too long, max len 31")); } for (index, letter) in name.chars().enumerate() { // no uppercase characters allowed if letter.is_ascii_uppercase() { return Some(String::from("Has uppercase")); } if index == 0 { // first character ahs to be either a letter or underscore if !(letter.is_ascii_alphabetic() || letter == '_') { return Some(String::from("Does not start with letter or _")); } } else { // after first character options are more relaxed if !(letter.is_ascii_alphabetic() || letter.is_ascii_digit() || letter == '_' || letter == '-') { return Some(String::from("Contains character that is not letter, number, _ or -")); } } } None } async fn db_get_user(pool: &Pool, auth: &str) -> Option { if let Ok(res) = sqlx::query_as::<_, AccountsNew>( r#" SELECT * FROM accounts_new WHERE auth_code == ? "#, ) .bind(auth) .fetch_all(pool) .await { if !res.is_empty() { return Some(res[0].to_owned()); } } None } async fn ldap_create_account(config: &Config, db: &Pool, user: AccountsNew, username: &str, pass: &str) -> Result<(), ldap3::LdapError> { let mut ldap = LdapConn::new(&config.ldap_host)?; ldap.simple_bind(&config.ldap_admin, &config.ldap_admin_pw)?.success()?; let dn = format!("uid={},ou=users,dc=skynet,dc=ie", username); let cn = format!("{} {}", &user.name_first, &user.name_surname); let home_directory = format!("/home/{}", username); let password_tmp = random_string(50); let labeled_uri = format!("ldap:///ou=groups,dc=skynet,dc=ie??sub?(&(objectclass=posixgroup)(memberuid={}))", username); let sk_mail = format!("{}@skynet.ie", username); let sk_created = get_sk_created(); let uid_number = get_max_uid_number(db).await; // create user ldap.add( &dn, vec![ ("objectClass", HashSet::from(["top", "person", "posixaccount", "ldapPublicKey", "inetOrgPerson", "skPerson"])), // top ("ou", HashSet::from(["users"])), // person ("uid", HashSet::from([username])), ("cn", HashSet::from([cn.as_str()])), // posixaccount ("uidNumber", HashSet::from([uid_number.to_string().as_str()])), ("gidNumber", HashSet::from(["1001"])), ("homedirectory", HashSet::from([home_directory.as_str()])), ("userpassword", HashSet::from([password_tmp.as_str()])), // inetOrgPerson ("mail", HashSet::from([user.mail.as_str()])), ("sn", HashSet::from([user.name_surname.as_str()])), // skPerson ("labeledURI", HashSet::from([labeled_uri.as_str()])), ("skMail", HashSet::from([sk_mail.as_str()])), ("skID", HashSet::from([user.id_student.as_str()])), ("skCreated", HashSet::from([sk_created.as_str()])), // 1 = secure, automatic since its a new account ("skSecure", HashSet::from(["1"])), // quotas ("quotaEmail", HashSet::from(["10737418240"])), ("quotaDisk", HashSet::from(["10737418240"])), ], )? .success()?; // now to properly set teh password let tmp = PasswordModify { user_id: Some(&dn), old_pass: None, new_pass: Some(pass), }; ldap.extended(tmp).unwrap(); // user is already verified by being an active member on wolves if let Err(e) = update_group(config, "skynet-users", &vec![username.to_string()], false).await { println!("Couldnt add {} to skynet-users: {:?}", username, e) } ldap.unbind()?; Ok(()) } fn get_sk_created() -> String { use chrono::Utc; let now = Utc::now(); format!("{}", now.format("%Y%m%d%H%M%SZ")) } async fn get_max_uid_number(db: &Pool) -> i64 { if let Ok(results) = sqlx::query_as::<_, Accounts>( r#" SELECT * FROM accounts ORDER BY uid DESC LIMIT 1 "#, ) .fetch_all(db) .await { if !results.is_empty() { return results[0].uid + 1; } } 9999 } async fn account_verification_clear_pending(db: &Pool, auth_code: &str) -> Result, Error> { sqlx::query_as::<_, AccountsNew>( r#" DELETE FROM accounts_new WHERE auth_code == ? "#, ) .bind(auth_code) .fetch_all(db) .await } }