diff --git a/.rustfmt.toml b/.rustfmt.toml index aacafc1..b8ae8dd 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,6 +1,9 @@ -max_width = 1000 +max_width = 150 single_line_if_else_max_width = 100 chain_width = 100 fn_params_layout = "Compressed" #control_brace_style = "ClosingNextLine" -struct_lit_width = 0 \ No newline at end of file +#brace_style = "PreferSameLine" +struct_lit_width = 0 +tab_spaces = 2 +use_small_heuristics = "Max" \ No newline at end of file diff --git a/src/bin/update_data.rs b/src/bin/update_data.rs index f42cde5..a504aad 100644 --- a/src/bin/update_data.rs +++ b/src/bin/update_data.rs @@ -4,161 +4,169 @@ use sqlx::{Pool, Sqlite}; #[async_std::main] async fn main() -> tide::Result<()> { - let config = get_config(); - let db = db_init(&config).await.unwrap(); + let config = get_config(); + let db = db_init(&config).await.unwrap(); - update_wolves(&config, &db).await; - update_ldap(&config, &db).await; + update_wolves(&config, &db).await; + update_ldap(&config, &db).await; - Ok(()) + Ok(()) } async fn update_wolves(config: &Config, db: &Pool) { - let mut records = vec![]; + let mut records = vec![]; - if let Ok(accounts) = get_csv(config) { - for account in accounts { - records.push(AccountWolves::from(account)); - } + if let Ok(accounts) = get_csv(config) { + for account in accounts { + records.push(AccountWolves::from(account)); } + } - for account in records { - update_account(db, &account).await; - } + for account in records { + update_account(db, &account).await; + } } async fn update_ldap(config: &Config, db: &Pool) { - let mut ldap = LdapConn::new(&config.ldap_host).unwrap(); + let mut ldap = LdapConn::new(&config.ldap_host).unwrap(); - ldap.simple_bind(&config.ldap_admin, &config.ldap_admin_pw).unwrap().success().unwrap(); + ldap.simple_bind(&config.ldap_admin, &config.ldap_admin_pw).unwrap().success().unwrap(); - // use this to pre load a large chunk of data - if let Ok(x) = ldap.search("ou=users,dc=skynet,dc=ie", Scope::OneLevel, "(objectClass=*)", vec!["uid", "uidNumber", "skDiscord", "skMemberOf", "mail", "skID", "userPassword"]) { - if let Ok((rs, _res)) = x.success() { - for entry in rs { - let tmp = SearchEntry::construct(entry); + // use this to pre load a large chunk of data + if let Ok(x) = ldap.search( + "ou=users,dc=skynet,dc=ie", + Scope::OneLevel, + "(objectClass=*)", + vec!["uid", "uidNumber", "skDiscord", "skMemberOf", "mail", "skID", "userPassword"], + ) { + if let Ok((rs, _res)) = x.success() { + for entry in rs { + let tmp = SearchEntry::construct(entry); - let mut tmp_account = Accounts { - user: "".to_string(), - uid: 0, - discord: None, - mail: "".to_string(), - student_id: "".to_string(), - enabled: false, - secure: false, - }; + let mut tmp_account = Accounts { + user: "".to_string(), + uid: 0, + discord: None, + mail: "".to_string(), + student_id: "".to_string(), + enabled: false, + secure: false, + }; - // pull out the required info - if tmp.attrs.contains_key("uid") && !tmp.attrs["uid"].is_empty() { - tmp_account.user = tmp.attrs["uid"][0].clone(); - } - if tmp.attrs.contains_key("uidNumber") && !tmp.attrs["uidNumber"].is_empty() { - tmp_account.uid = tmp.attrs["uidNumber"][0].clone().parse().unwrap_or(0); - } - if tmp.attrs.contains_key("skDiscord") && !tmp.attrs["skDiscord"].is_empty() { - tmp_account.discord = Option::from(tmp.attrs["skDiscord"][0].clone()); - } - if tmp.attrs.contains_key("mail") && !tmp.attrs["mail"].is_empty() { - tmp_account.mail = tmp.attrs["mail"][0].clone(); - } - if tmp.attrs.contains_key("skID") && !tmp.attrs["skID"].is_empty() { - tmp_account.student_id = tmp.attrs["skID"][0].clone(); - } - if tmp.attrs.contains_key("skMemberOf") && !tmp.attrs["skMemberOf"].is_empty() && tmp.attrs["skMemberOf"].contains(&String::from("cn=skynet-users-linux,ou=groups,dc=skynet,dc=ie")) { - tmp_account.enabled = true; - } - if tmp.attrs.contains_key("userPassword") && !tmp.attrs["userPassword"].is_empty() { - tmp_account.secure = tmp.attrs["userPassword"][0].starts_with("{SSHA512}") - } + // pull out the required info + if tmp.attrs.contains_key("uid") && !tmp.attrs["uid"].is_empty() { + tmp_account.user = tmp.attrs["uid"][0].clone(); + } + if tmp.attrs.contains_key("uidNumber") && !tmp.attrs["uidNumber"].is_empty() { + tmp_account.uid = tmp.attrs["uidNumber"][0].clone().parse().unwrap_or(0); + } + if tmp.attrs.contains_key("skDiscord") && !tmp.attrs["skDiscord"].is_empty() { + tmp_account.discord = Option::from(tmp.attrs["skDiscord"][0].clone()); + } + if tmp.attrs.contains_key("mail") && !tmp.attrs["mail"].is_empty() { + tmp_account.mail = tmp.attrs["mail"][0].clone(); + } + if tmp.attrs.contains_key("skID") && !tmp.attrs["skID"].is_empty() { + tmp_account.student_id = tmp.attrs["skID"][0].clone(); + } + if tmp.attrs.contains_key("skMemberOf") + && !tmp.attrs["skMemberOf"].is_empty() + && tmp.attrs["skMemberOf"].contains(&String::from("cn=skynet-users-linux,ou=groups,dc=skynet,dc=ie")) + { + tmp_account.enabled = true; + } + if tmp.attrs.contains_key("userPassword") && !tmp.attrs["userPassword"].is_empty() { + tmp_account.secure = tmp.attrs["userPassword"][0].starts_with("{SSHA512}") + } - if !tmp_account.user.is_empty() { - sqlx::query_as::<_, Accounts>( - " + if !tmp_account.user.is_empty() { + sqlx::query_as::<_, Accounts>( + " INSERT OR REPLACE INTO accounts (user, uid, discord, mail, student_id, enabled, secure) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ", - ) - .bind(&tmp_account.user) - .bind(tmp_account.uid) - .bind(&tmp_account.discord) - .bind(&tmp_account.mail) - .bind(&tmp_account.student_id) - .bind(tmp_account.enabled) - .bind(tmp_account.secure) - .fetch_optional(db) - .await - .ok(); - } - } + ) + .bind(&tmp_account.user) + .bind(tmp_account.uid) + .bind(&tmp_account.discord) + .bind(&tmp_account.mail) + .bind(&tmp_account.student_id) + .bind(tmp_account.enabled) + .bind(tmp_account.secure) + .fetch_optional(db) + .await + .ok(); } + } } + } - // done with ldap - ldap.unbind().unwrap(); + // done with ldap + ldap.unbind().unwrap(); } #[derive(Debug, serde::Deserialize)] struct RecordCSV { - #[serde(rename = "MemID")] - mem_id: String, - #[serde(rename = "Student Num")] - id_student: String, - #[serde(rename = "Contact Email")] - email: String, - #[serde(rename = "Expiry")] - expiry: String, - #[serde(rename = "First Name")] - name_first: String, - #[serde(rename = "Last Name")] - name_second: String, + #[serde(rename = "MemID")] + mem_id: String, + #[serde(rename = "Student Num")] + id_student: String, + #[serde(rename = "Contact Email")] + email: String, + #[serde(rename = "Expiry")] + expiry: String, + #[serde(rename = "First Name")] + name_first: String, + #[serde(rename = "Last Name")] + name_second: String, } impl From for AccountWolves { - fn from(input: RecordCSV) -> Self { - AccountWolves { - id_wolves: input.mem_id, - id_student: input.id_student, - email: input.email, - expiry: input.expiry, - name_first: input.name_first, - name_second: input.name_second, - } + fn from(input: RecordCSV) -> Self { + AccountWolves { + id_wolves: input.mem_id, + id_student: input.id_student, + email: input.email, + expiry: input.expiry, + name_first: input.name_first, + name_second: input.name_second, } + } } fn get_csv(config: &Config) -> Result, Box> { - let mut records: Vec = vec![]; + let mut records: Vec = vec![]; - let csv = format!("{}/{}", &config.home, &config.csv); - println!("CSV: {:?}", &csv); - if let Ok(mut rdr) = csv::Reader::from_path(csv) { - for result in rdr.deserialize() { - // Notice that we need to provide a type hint for automatic - // deserialization. - let record: RecordCSV = result?; - if record.mem_id.is_empty() { - continue; - } - records.push(record); - } + let csv = format!("{}/{}", &config.home, &config.csv); + println!("CSV: {:?}", &csv); + if let Ok(mut rdr) = csv::Reader::from_path(csv) { + for result in rdr.deserialize() { + // Notice that we need to provide a type hint for automatic + // deserialization. + let record: RecordCSV = result?; + if record.mem_id.is_empty() { + continue; + } + records.push(record); } + } - Ok(records) + Ok(records) } async fn update_account(db: &Pool, account: &AccountWolves) { - sqlx::query_as::<_, AccountWolves>( - " + sqlx::query_as::<_, AccountWolves>( + " INSERT OR REPLACE INTO accounts_wolves (id_wolves, id_student, email, expiry, name_first, name_second) VALUES (?1, ?2, ?3, ?4, ?5, ?6) ", - ) - .bind(&account.id_wolves) - .bind(&account.id_student) - .bind(&account.email) - .bind(&account.expiry) - .bind(&account.name_first) - .bind(&account.name_second) - .fetch_optional(db) - .await - .ok(); + ) + .bind(&account.id_wolves) + .bind(&account.id_student) + .bind(&account.email) + .bind(&account.expiry) + .bind(&account.name_first) + .bind(&account.name_second) + .fetch_optional(db) + .await + .ok(); } diff --git a/src/bin/update_groups.rs b/src/bin/update_groups.rs index 3eb8b23..bbbf23d 100644 --- a/src/bin/update_groups.rs +++ b/src/bin/update_groups.rs @@ -4,170 +4,170 @@ use std::{collections::HashSet, env, error::Error}; #[async_std::main] async fn main() -> tide::Result<()> { - let config = get_config(); + let config = get_config(); - update(&config).await?; + update(&config).await?; - Ok(()) + Ok(()) } async fn update(config: &Config) -> tide::Result<()> { - let db = db_init(config).await.unwrap(); + let db = db_init(config).await.unwrap(); - // default user to ensure group is never empty - let mut users_tmp = HashSet::from([String::from("compsoc")]); - let mut admins_tmp = HashSet::from([String::from("compsoc")]); - let mut committee_tmp = HashSet::from([String::from("compsoc")]); + // default user to ensure group is never empty + let mut users_tmp = HashSet::from([String::from("compsoc")]); + let mut admins_tmp = HashSet::from([String::from("compsoc")]); + let mut committee_tmp = HashSet::from([String::from("compsoc")]); - if let Ok(x) = env::var("USERS_LIFETIME") { - for user in x.split(',').collect::>() { - users_tmp.insert(user.to_string()); - } + if let Ok(x) = env::var("USERS_LIFETIME") { + for user in x.split(',').collect::>() { + users_tmp.insert(user.to_string()); } + } - // pull from wolves csv - for user in from_csv(&db).await.unwrap_or_default() { - users_tmp.insert(user); + // pull from wolves csv + for user in from_csv(&db).await.unwrap_or_default() { + users_tmp.insert(user); + } + + if let Ok(x) = env::var("USERS_ADMIN") { + // admins automatically get added as users + for user in x.split(',').collect::>() { + admins_tmp.insert(user.to_string()); + users_tmp.insert(user.to_string()); } + } - if let Ok(x) = env::var("USERS_ADMIN") { - // admins automatically get added as users - for user in x.split(',').collect::>() { - admins_tmp.insert(user.to_string()); - users_tmp.insert(user.to_string()); - } + // read from teh env + if let Ok(x) = env::var("USERS_COMMITTEE") { + // committee automatically get added as users + for user in x.split(',').collect::>() { + committee_tmp.insert(user.to_string()); + users_tmp.insert(user.to_string()); } + } - // read from teh env - if let Ok(x) = env::var("USERS_COMMITTEE") { - // committee automatically get added as users - for user in x.split(',').collect::>() { - committee_tmp.insert(user.to_string()); - users_tmp.insert(user.to_string()); - } + // sorting makes it easier/faster + if let Ok(x) = env::var("USERS_BANNED") { + for user in x.split(',').collect::>() { + users_tmp.remove(user); } + } - // sorting makes it easier/faster - if let Ok(x) = env::var("USERS_BANNED") { - for user in x.split(',').collect::>() { - users_tmp.remove(user); - } - } + let AccountsSecure { + users, + admins, + committee, + } = get_secure(&db, &users_tmp, &admins_tmp, &committee_tmp).await; - let AccountsSecure { - users, - admins, - committee, - } = get_secure(&db, &users_tmp, &admins_tmp, &committee_tmp).await; + update_group(config, "skynet-users", &users, true).await?; + update_group(config, "skynet-admins", &admins, true).await?; + update_group(config, "skynet-committee", &committee, true).await?; - update_group(config, "skynet-users", &users, true).await?; - update_group(config, "skynet-admins", &admins, true).await?; - update_group(config, "skynet-committee", &committee, true).await?; - - Ok(()) + Ok(()) } async fn from_csv(db: &Pool) -> Result, Box> { - let mut uids = HashSet::new(); + let mut uids = HashSet::new(); - for record in get_wolves(db).await { - // only import users if it is actually active. - if record.expiry < get_now_iso(true) { - continue; - } - if let Some(uid) = account_mail_get_uid(db, &record.email).await { - uids.insert(uid); - } else if let Some(uid) = account_id_get_uid(db, &record.id_student).await { - uids.insert(uid); - } + for record in get_wolves(db).await { + // only import users if it is actually active. + if record.expiry < get_now_iso(true) { + continue; } + if let Some(uid) = account_mail_get_uid(db, &record.email).await { + uids.insert(uid); + } else if let Some(uid) = account_id_get_uid(db, &record.id_student).await { + uids.insert(uid); + } + } - Ok(uids) + Ok(uids) } async fn account_mail_get_uid(db: &Pool, mail: &str) -> Option { - match sqlx::query_as::<_, Accounts>( - r#" + match sqlx::query_as::<_, Accounts>( + r#" SELECT * FROM accounts WHERE mail == ? "#, - ) - .bind(mail) - .fetch_one(db) - .await - { - Ok(res) => Some(res.user.to_owned()), - Err(_) => None, - } + ) + .bind(mail) + .fetch_one(db) + .await + { + Ok(res) => Some(res.user.to_owned()), + Err(_) => None, + } } async fn account_id_get_uid(db: &Pool, id: &str) -> Option { - match sqlx::query_as::<_, Accounts>( - r#" + match sqlx::query_as::<_, Accounts>( + r#" SELECT * FROM accounts WHERE student_id == ? "#, - ) - .bind(id) - .fetch_one(db) - .await - { - Ok(res) => Some(res.student_id.to_owned()), - Err(_) => None, - } + ) + .bind(id) + .fetch_one(db) + .await + { + Ok(res) => Some(res.student_id.to_owned()), + Err(_) => None, + } } struct AccountsSecure { - users: Vec, - admins: Vec, - committee: Vec, + users: Vec, + admins: Vec, + committee: Vec, } async fn get_secure(db: &Pool, users: &HashSet, admins: &HashSet, committee: &HashSet) -> AccountsSecure { - // to avoid searching for teh same thing again. - let mut cache = HashSet::new(); - AccountsSecure { - users: get_secure_sub(db, users, &mut cache).await, - admins: get_secure_sub(db, admins, &mut cache).await, - committee: get_secure_sub(db, committee, &mut cache).await, - } + // to avoid searching for teh same thing again. + let mut cache = HashSet::new(); + AccountsSecure { + users: get_secure_sub(db, users, &mut cache).await, + admins: get_secure_sub(db, admins, &mut cache).await, + committee: get_secure_sub(db, committee, &mut cache).await, + } } async fn get_secure_sub(db: &Pool, group: &HashSet, cache: &mut HashSet) -> Vec { - let mut tmp = vec![]; + let mut tmp = vec![]; - for user in group { - // check the cache first - let mut add = false; - if cache.get(user).is_some() { - add = true; - } else if is_secure(db, user).await { - cache.insert(user.to_string()); - add = true; - } - - if add { - tmp.push(user.clone()); - } + for user in group { + // check the cache first + let mut add = false; + if cache.get(user).is_some() { + add = true; + } else if is_secure(db, user).await { + cache.insert(user.to_string()); + add = true; } - tmp + if add { + tmp.push(user.clone()); + } + } + + tmp } async fn is_secure(db: &Pool, user: &str) -> bool { - match sqlx::query_as::<_, Accounts>( - r#" + match sqlx::query_as::<_, Accounts>( + r#" SELECT * FROM accounts WHERE user == ? AND secure == 1 "#, - ) - .bind(user) - .fetch_all(db) - .await - { - Ok(res) => !res.is_empty(), - Err(_) => false, - } + ) + .bind(user) + .fetch_all(db) + .await + { + Ok(res) => !res.is_empty(), + Err(_) => false, + } } diff --git a/src/lib.rs b/src/lib.rs index 7e730dc..ca49af2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,72 +4,72 @@ use dotenvy::dotenv; use ldap3::{LdapConn, Mod}; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use sqlx::{ - sqlite::{SqliteConnectOptions, SqlitePoolOptions}, - Error, Pool, Sqlite, + sqlite::{SqliteConnectOptions, SqlitePoolOptions}, + Error, Pool, Sqlite, }; use std::{ - env, - str::FromStr, - time::{SystemTime, UNIX_EPOCH}, + env, + str::FromStr, + time::{SystemTime, UNIX_EPOCH}, }; use tide::prelude::*; #[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] pub struct AccountWolves { - pub id_wolves: String, - pub id_student: String, - pub email: String, - pub expiry: String, - pub name_first: String, - pub name_second: String, + pub id_wolves: String, + pub id_student: String, + pub email: String, + pub expiry: String, + pub name_first: String, + pub name_second: String, } #[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)] pub struct AccountsNew { - pub mail: String, - pub auth_code: String, - pub date_iso: String, - pub date_expiry: String, - pub name_first: String, - pub name_surname: String, - pub id_student: String, + pub mail: String, + pub auth_code: String, + pub date_iso: String, + pub date_expiry: String, + pub name_first: String, + pub name_surname: String, + pub id_student: String, } #[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)] pub struct AccountsReset { - pub user: String, - pub auth_code: String, - pub date_expiry: String, + pub user: String, + pub auth_code: String, + pub date_expiry: String, } #[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)] pub struct AccountsSSH { - pub user: String, - pub auth_code: String, - pub email: String, + pub user: String, + pub auth_code: String, + pub email: String, } #[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)] pub struct Accounts { - pub user: String, - pub uid: i64, - pub discord: Option, - pub mail: String, - pub student_id: String, - pub enabled: bool, - pub secure: bool, + pub user: String, + pub uid: i64, + pub discord: Option, + pub mail: String, + pub student_id: String, + pub enabled: bool, + pub secure: bool, } pub async fn db_init(config: &Config) -> Result, Error> { - let database = format!("{}/{}", &config.home, &config.database); - println!("Database: {:?}", &database); - let pool = SqlitePoolOptions::new() - .max_connections(5) - .connect_with(SqliteConnectOptions::from_str(&format!("sqlite://{}", database))?.create_if_missing(true)) - .await?; + let database = format!("{}/{}", &config.home, &config.database); + println!("Database: {:?}", &database); + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect_with(SqliteConnectOptions::from_str(&format!("sqlite://{}", database))?.create_if_missing(true)) + .await?; - sqlx::query( - "CREATE TABLE IF NOT EXISTS accounts_wolves ( + sqlx::query( + "CREATE TABLE IF NOT EXISTS accounts_wolves ( id_wolves text primary key, id_student text not null, email text not null, @@ -77,12 +77,12 @@ pub async fn db_init(config: &Config) -> Result, Error> { name_first text not null, name_second integer not null )", - ) - .execute(&pool) - .await?; + ) + .execute(&pool) + .await?; - sqlx::query( - "CREATE TABLE IF NOT EXISTS accounts_new ( + sqlx::query( + "CREATE TABLE IF NOT EXISTS accounts_new ( mail text primary key, auth_code text not null, date_iso text not null, @@ -91,47 +91,47 @@ pub async fn db_init(config: &Config) -> Result, Error> { name_surname integer not null, id_student text not null )", - ) + ) + .execute(&pool) + .await?; + + sqlx::query("CREATE INDEX IF NOT EXISTS index_auth_code ON accounts_new (auth_code)") + .execute(&pool) + .await?; + sqlx::query("CREATE INDEX IF NOT EXISTS index_date_expiry ON accounts_new (date_expiry)") .execute(&pool) .await?; - sqlx::query("CREATE INDEX IF NOT EXISTS index_auth_code ON accounts_new (auth_code)") - .execute(&pool) - .await?; - sqlx::query("CREATE INDEX IF NOT EXISTS index_date_expiry ON accounts_new (date_expiry)") - .execute(&pool) - .await?; - - sqlx::query( - "CREATE TABLE IF NOT EXISTS accounts_ssh ( + sqlx::query( + "CREATE TABLE IF NOT EXISTS accounts_ssh ( user text primary key, auth_code text not null, email text not null )", - ) - .execute(&pool) - .await?; + ) + .execute(&pool) + .await?; - sqlx::query( - "CREATE TABLE IF NOT EXISTS accounts_reset ( + sqlx::query( + "CREATE TABLE IF NOT EXISTS accounts_reset ( user text primary key, auth_code text not null, date_expiry text not null )", - ) + ) + .execute(&pool) + .await?; + + sqlx::query("CREATE INDEX IF NOT EXISTS index_auth_code ON accounts_reset (auth_code)") + .execute(&pool) + .await?; + sqlx::query("CREATE INDEX IF NOT EXISTS index_date_expiry ON accounts_reset (date_expiry)") .execute(&pool) .await?; - sqlx::query("CREATE INDEX IF NOT EXISTS index_auth_code ON accounts_reset (auth_code)") - .execute(&pool) - .await?; - sqlx::query("CREATE INDEX IF NOT EXISTS index_date_expiry ON accounts_reset (date_expiry)") - .execute(&pool) - .await?; - - // this is for active use - sqlx::query( - "CREATE TABLE IF NOT EXISTS accounts ( + // this is for active use + sqlx::query( + "CREATE TABLE IF NOT EXISTS accounts ( user text primary key, uid integer not null, discord text, @@ -140,168 +140,168 @@ pub async fn db_init(config: &Config) -> Result, Error> { enabled integer not null, secure integer not null )", - ) + ) + .execute(&pool) + .await?; + + sqlx::query("CREATE INDEX IF NOT EXISTS index_uid_number ON accounts (uid)").execute(&pool).await?; + sqlx::query("CREATE INDEX IF NOT EXISTS index_mail ON accounts (mail)").execute(&pool).await?; + sqlx::query("CREATE INDEX IF NOT EXISTS index_student_id ON accounts (student_id)") .execute(&pool) .await?; - sqlx::query("CREATE INDEX IF NOT EXISTS index_uid_number ON accounts (uid)").execute(&pool).await?; - sqlx::query("CREATE INDEX IF NOT EXISTS index_mail ON accounts (mail)").execute(&pool).await?; - sqlx::query("CREATE INDEX IF NOT EXISTS index_student_id ON accounts (student_id)") - .execute(&pool) - .await?; - - Ok(pool) + Ok(pool) } pub fn get_now() -> i64 { - if let Ok(x) = SystemTime::now().duration_since(UNIX_EPOCH) { - x.as_secs() as i64 - } else { - 0 - } + if let Ok(x) = SystemTime::now().duration_since(UNIX_EPOCH) { + x.as_secs() as i64 + } else { + 0 + } } pub fn get_now_iso(short: bool) -> String { - let now = Utc::now(); - if short { - format!("{}-{:02}-{:02}", now.year(), now.month(), now.day()) - } else { - now.to_rfc3339_opts(SecondsFormat::Millis, true) - } + let now = Utc::now(); + if short { + format!("{}-{:02}-{:02}", now.year(), now.month(), now.day()) + } else { + now.to_rfc3339_opts(SecondsFormat::Millis, true) + } } #[derive(Clone)] pub struct State { - pub db: Pool, - pub config: Config, + pub db: Pool, + pub config: Config, } #[derive(Debug, Clone)] pub struct Config { - pub ldap_host: String, - pub ldap_admin: String, - pub ldap_admin_pw: String, - pub home: String, - pub database: String, - pub csv: String, - pub host_port: String, - pub mail_smtp: String, - pub mail_user: String, - pub mail_pass: String, - pub ssh_root: String, + pub ldap_host: String, + pub ldap_admin: String, + pub ldap_admin_pw: String, + pub home: String, + pub database: String, + pub csv: String, + pub host_port: String, + pub mail_smtp: String, + pub mail_user: String, + pub mail_pass: String, + pub ssh_root: String, } pub fn get_config() -> Config { - dotenv().ok(); + dotenv().ok(); - // reasonable defaults - let mut config = Config { - ldap_host: "".to_string(), - ldap_admin: "".to_string(), - ldap_admin_pw: "".to_string(), - home: ".".to_string(), - database: "database.db".to_string(), - csv: "wolves.csv".to_string(), - host_port: "127.0.0.1:8087".to_string(), - mail_smtp: "".to_string(), - mail_user: "".to_string(), - mail_pass: "".to_string(), - ssh_root: "/skynet_old/home".to_string(), - }; + // reasonable defaults + let mut config = Config { + ldap_host: "".to_string(), + ldap_admin: "".to_string(), + ldap_admin_pw: "".to_string(), + home: ".".to_string(), + database: "database.db".to_string(), + csv: "wolves.csv".to_string(), + host_port: "127.0.0.1:8087".to_string(), + mail_smtp: "".to_string(), + mail_user: "".to_string(), + mail_pass: "".to_string(), + ssh_root: "/skynet_old/home".to_string(), + }; - if let Ok(x) = env::var("LDAP_HOST") { - config.ldap_host = x.trim().to_string(); - } - if let Ok(x) = env::var("LDAP_ADMIN") { - config.ldap_admin = x.trim().to_string(); - } - if let Ok(x) = env::var("LDAP_ADMIN_PW") { - config.ldap_admin_pw = x.trim().to_string(); - } - if let Ok(x) = env::var("HOME") { - config.home = x.trim().to_string(); - } - if let Ok(x) = env::var("DATABASE") { - config.database = x.trim().to_string(); - } - if let Ok(x) = env::var("CSV") { - config.csv = x.trim().to_string(); - } - if let Ok(x) = env::var("HOST_PORT") { - config.host_port = x.trim().to_string(); - } - if let Ok(x) = env::var("EMAIL_SMTP") { - config.mail_smtp = x.trim().to_string(); - } - if let Ok(x) = env::var("EMAIL_USER") { - config.mail_user = x.trim().to_string(); - } - if let Ok(x) = env::var("EMAIL_PASS") { - config.mail_pass = x.trim().to_string(); - } - if let Ok(x) = env::var("SSH_ROOT") { - config.ssh_root = x.trim().to_string(); - } + if let Ok(x) = env::var("LDAP_HOST") { + config.ldap_host = x.trim().to_string(); + } + if let Ok(x) = env::var("LDAP_ADMIN") { + config.ldap_admin = x.trim().to_string(); + } + if let Ok(x) = env::var("LDAP_ADMIN_PW") { + config.ldap_admin_pw = x.trim().to_string(); + } + if let Ok(x) = env::var("HOME") { + config.home = x.trim().to_string(); + } + if let Ok(x) = env::var("DATABASE") { + config.database = x.trim().to_string(); + } + if let Ok(x) = env::var("CSV") { + config.csv = x.trim().to_string(); + } + if let Ok(x) = env::var("HOST_PORT") { + config.host_port = x.trim().to_string(); + } + if let Ok(x) = env::var("EMAIL_SMTP") { + config.mail_smtp = x.trim().to_string(); + } + if let Ok(x) = env::var("EMAIL_USER") { + config.mail_user = x.trim().to_string(); + } + if let Ok(x) = env::var("EMAIL_PASS") { + config.mail_pass = x.trim().to_string(); + } + if let Ok(x) = env::var("SSH_ROOT") { + config.ssh_root = x.trim().to_string(); + } - config + config } // from https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html#create-random-passwords-from-a-set-of-alphanumeric-characters pub fn random_string(len: usize) -> String { - thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect() + thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect() } pub async fn get_wolves(db: &Pool) -> Vec { - sqlx::query_as::<_, AccountWolves>( - r#" + sqlx::query_as::<_, AccountWolves>( + r#" SELECT * FROM accounts_wolves "#, - ) - .fetch_all(db) - .await - .unwrap_or(vec![]) + ) + .fetch_all(db) + .await + .unwrap_or(vec![]) } pub fn uid_to_dn(uid: &str) -> String { - format!("uid={},ou=users,dc=skynet,dc=ie", uid) + format!("uid={},ou=users,dc=skynet,dc=ie", uid) } pub async fn update_group(config: &Config, group: &str, users: &Vec, replace: bool) -> tide::Result<()> { - if users.is_empty() { - return Ok(()); - } + if users.is_empty() { + return Ok(()); + } - let mut ldap = LdapConn::new(&config.ldap_host)?; + let mut ldap = LdapConn::new(&config.ldap_host)?; - // use the admin account - ldap.simple_bind(&config.ldap_admin, &config.ldap_admin_pw)?.success()?; + // use the admin account + ldap.simple_bind(&config.ldap_admin, &config.ldap_admin_pw)?.success()?; - let dn = format!("cn={},ou=groups,dc=skynet,dc=ie", group); - let members = users.iter().map(|uid| uid_to_dn(uid)).collect(); - let mods = if replace { - vec![Mod::Replace("member".to_string(), members)] - } else { - vec![Mod::Add("member".to_string(), members)] - }; + let dn = format!("cn={},ou=groups,dc=skynet,dc=ie", group); + let members = users.iter().map(|uid| uid_to_dn(uid)).collect(); + let mods = if replace { + vec![Mod::Replace("member".to_string(), members)] + } else { + vec![Mod::Add("member".to_string(), members)] + }; - if let Err(x) = ldap.modify(&dn, mods) { - println!("{:?}", x); - } + if let Err(x) = ldap.modify(&dn, mods) { + println!("{:?}", x); + } - let dn_linux = format!("cn={}-linux,ou=groups,dc=skynet,dc=ie", group); - let members_linux = users.iter().map(|uid| uid.to_string()).collect(); - let mods = if replace { - vec![Mod::Replace("memberUid".to_string(), members_linux)] - } else { - vec![Mod::Add("memberUid".to_string(), members_linux)] - }; - if let Err(x) = ldap.modify(&dn_linux, mods) { - println!("{:?}", x); - }; + let dn_linux = format!("cn={}-linux,ou=groups,dc=skynet,dc=ie", group); + let members_linux = users.iter().map(|uid| uid.to_string()).collect(); + let mods = if replace { + vec![Mod::Replace("memberUid".to_string(), members_linux)] + } else { + vec![Mod::Add("memberUid".to_string(), members_linux)] + }; + if let Err(x) = ldap.modify(&dn_linux, mods) { + println!("{:?}", x); + }; - // tidy up - ldap.unbind()?; + // tidy up + ldap.unbind()?; - Ok(()) + Ok(()) } diff --git a/src/main.rs b/src/main.rs index b7ddedf..262d918 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,39 +1,39 @@ use skynet_ldap_backend::{ - db_init, get_config, - methods::{account_new, account_recover, account_update}, - State, + db_init, get_config, + methods::{account_new, account_recover, account_update}, + State, }; #[async_std::main] async fn main() -> tide::Result<()> { - let config = get_config(); - let db = db_init(&config).await?; + let config = get_config(); + let db = db_init(&config).await?; - let host_port = config.host_port.clone(); + let host_port = config.host_port.clone(); - tide::log::start(); + tide::log::start(); - let state = State { - db, - config, - }; + let state = State { + db, + config, + }; - let mut app = tide::with_state(state); + let mut app = tide::with_state(state); - // for users to update their own profile - app.at("/ldap/update").post(account_update::submit); + // for users to update their own profile + app.at("/ldap/update").post(account_update::submit); - // for new users - app.at("/ldap/new/email").post(account_new::email::submit); - app.at("/ldap/new/account").post(account_new::account::submit); + // for new users + app.at("/ldap/new/email").post(account_new::email::submit); + app.at("/ldap/new/account").post(account_new::account::submit); - // for folks who forget password/username - app.at("/ldap/recover/password").post(account_recover::password::reset); - app.at("/ldap/recover/password/auth").post(account_recover::password::auth); - app.at("/ldap/recover/username").post(account_recover::username::submit); - app.at("/ldap/recover/ssh/request").post(account_recover::ssh::request); - app.at("/ldap/recover/ssh/verify").post(account_recover::ssh::verify); + // for folks who forget password/username + app.at("/ldap/recover/password").post(account_recover::password::reset); + app.at("/ldap/recover/password/auth").post(account_recover::password::auth); + app.at("/ldap/recover/username").post(account_recover::username::submit); + app.at("/ldap/recover/ssh/request").post(account_recover::ssh::request); + app.at("/ldap/recover/ssh/verify").post(account_recover::ssh::verify); - app.listen(host_port).await?; - Ok(()) + app.listen(host_port).await?; + Ok(()) } diff --git a/src/methods/account_new.rs b/src/methods/account_new.rs index bf991df..012e4d9 100644 --- a/src/methods/account_new.rs +++ b/src/methods/account_new.rs @@ -1,170 +1,170 @@ 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, + message::{header, MultiPart, SinglePart}, + transport::smtp::{self, authentication::Credentials}, + Message, SmtpTransport, Transport, }; use maud::html; use sqlx::{Error, Pool, Sqlite}; use std::collections::HashSet; use tide::{ - prelude::{json, Deserialize}, - Request, + prelude::{json, Deserialize}, + Request, }; pub mod email { - use super::*; + use super::*; - #[derive(Debug, Deserialize)] - struct SignupEmail { - email: String, - } + #[derive(Debug, Deserialize)] + struct SignupEmail { + email: String, + } - pub async fn submit(mut req: Request) -> tide::Result { - let SignupEmail { - email, - } = req.body_json().await?; + 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; + 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; - } + 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; - } + // 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); + // 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); - } - } + 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#" + 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![]) - } + ) + .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#" + 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#" + ) + .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() - } + ) + .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); + // 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; }" - } + // 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" + } + 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) } } - }; - let body_text = format!( - r#" + 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. @@ -186,275 +186,276 @@ pub mod email { 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"); + // 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()); + 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(); + // 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) - } + // Send the email + mailer.send(&email) + } - async fn save_to_db(db: &Pool, record: &AccountWolves, auth: &str) -> Result, sqlx::Error> { - sqlx::query_as::<_, AccountsNew>( - " + 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 - } + ) + .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; + use super::*; + use crate::update_group; - #[derive(Debug, Deserialize)] - struct LdapNewUser { - auth: String, - user: String, - pass: String, + #[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()); } - /// 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?; + // easier to give each request its own connection + let mut ldap = LdapConn::new(&config.ldap_host)?; - let config = &req.state().config; - let db = &req.state().db; + // ldap3 docs say a blank username and pass is an anon bind + ldap.simple_bind("", "")?.success()?; - // 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()); + 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()); } - - // 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#" + // 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 + ) + .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")); } - 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 _")); } - - 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 -")); - } - } + } 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#" + 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 + ) + .bind(auth) + .fetch_all(pool) + .await + { + if !res.is_empty() { + return Some(res[0].to_owned()); + } } - 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()?; + None + } - 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; + 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()?; - // 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()?; + 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; - // now to properly set teh password - let tmp = PasswordModify { - user_id: Some(&dn), - old_pass: None, - new_pass: Some(pass), - }; + // 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()?; - ldap.extended(tmp).unwrap(); + // now to properly set teh password + let tmp = PasswordModify { + user_id: Some(&dn), + old_pass: None, + new_pass: Some(pass), + }; - // 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.extended(tmp).unwrap(); - ldap.unbind()?; - - Ok(()) + // 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) } - fn get_sk_created() -> String { - use chrono::Utc; - let now = Utc::now(); + ldap.unbind()?; - format!("{}", now.format("%Y%m%d%H%M%SZ")) - } + Ok(()) + } - async fn get_max_uid_number(db: &Pool) -> i64 { - if let Ok(results) = sqlx::query_as::<_, Accounts>( - r#" + 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 + ) + .fetch_all(db) + .await + { + if !results.is_empty() { + return results[0].uid + 1; + } } - async fn account_verification_clear_pending(db: &Pool, auth_code: &str) -> Result, Error> { - sqlx::query_as::<_, AccountsNew>( - r#" + 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 - } + ) + .bind(auth_code) + .fetch_all(db) + .await + } } diff --git a/src/methods/account_recover.rs b/src/methods/account_recover.rs index eb0087f..f09b538 100644 --- a/src/methods/account_recover.rs +++ b/src/methods/account_recover.rs @@ -2,256 +2,256 @@ use crate::{get_now_iso, random_string, uid_to_dn, Accounts, AccountsReset, Conf use chrono::{Duration, SecondsFormat, Utc}; use ldap3::{exop::PasswordModify, LdapConn}; use lettre::{ - message::{header, MultiPart, SinglePart}, - transport::smtp::{authentication::Credentials, response::Response, Error}, - Message, SmtpTransport, Transport, + message::{header, MultiPart, SinglePart}, + transport::smtp::{authentication::Credentials, response::Response, Error}, + Message, SmtpTransport, Transport, }; use maud::html; use sqlx::{Pool, Sqlite}; use tide::{ - prelude::{json, Deserialize}, - Request, + prelude::{json, Deserialize}, + Request, }; pub mod password { - use super::*; + use super::*; - #[derive(Debug, Deserialize)] - struct PassReset { - user: Option, - email: Option, + #[derive(Debug, Deserialize)] + struct PassReset { + user: Option, + email: Option, + } + + /// Handles password resets + /// All responses are success, never want to leak info + pub async fn reset(mut req: Request) -> tide::Result { + let PassReset { + user, + email, + } = req.body_json().await?; + + // check that any mail is not using @skynet.ie + if let Some(mail) = &email { + if mail.trim().ends_with("@skynet.ie") { + // all responses from this are a success + return Ok(json!({"result": "error", "error": "Skynet email not permitted."}).into()); + } } - /// Handles password resets - /// All responses are success, never want to leak info - pub async fn reset(mut req: Request) -> tide::Result { - let PassReset { - user, - email, - } = req.body_json().await?; + let config = &req.state().config; + let db = &req.state().db; - // check that any mail is not using @skynet.ie - if let Some(mail) = &email { - if mail.trim().ends_with("@skynet.ie") { - // all responses from this are a success - return Ok(json!({"result": "error", "error": "Skynet email not permitted."}).into()); - } - } + // considering the local db is updated hourly (or less) use that instead of teh ldap for lookups + let user_details = match db_get_user(db, &user, &email).await { + None => { + return Ok(json!({"result": "success"}).into()); + } + Some(x) => x, + }; - let config = &req.state().config; - let db = &req.state().db; - - // considering the local db is updated hourly (or less) use that instead of teh ldap for lookups - let user_details = match db_get_user(db, &user, &email).await { - None => { - return Ok(json!({"result": "success"}).into()); - } - Some(x) => x, - }; - - // user does not have a different email address set - if user_details.mail.trim().ends_with("@skynet.ie") { - // not returning an error here as there is no need to let the person requesting what email the user has - return Ok(json!({"result": "success"}).into()); - } - - // check if a recent password reset request happened lately - db_pending_clear_expired(db).await?; - - if db_get_user_reset(db, &user_details.user).await.is_some() { - // reset already requested within timeframe - return Ok(json!({"result": "success"}).into()); - } - - // send mail - let auth = random_string(50); - - if send_mail(config, &user_details, &auth).is_ok() { - // save to db - - save_to_db(db, &user_details, &auth).await?; - } - - Ok(json!({"result": "success"}).into()) + // user does not have a different email address set + if user_details.mail.trim().ends_with("@skynet.ie") { + // not returning an error here as there is no need to let the person requesting what email the user has + return Ok(json!({"result": "success"}).into()); } - #[derive(Debug, Deserialize)] - pub struct PassResetAuth { - auth: String, - pass: String, + // check if a recent password reset request happened lately + db_pending_clear_expired(db).await?; + + if db_get_user_reset(db, &user_details.user).await.is_some() { + // reset already requested within timeframe + return Ok(json!({"result": "success"}).into()); } - pub async fn auth(mut req: Request) -> tide::Result { - let PassResetAuth { - auth, - pass, - } = req.body_json().await?; + // send mail + let auth = random_string(50); - let config = &req.state().config; - let db = &req.state().db; + if send_mail(config, &user_details, &auth).is_ok() { + // save to db - if db_pending_clear_expired(db).await.is_err() { - return Ok(json!({"result": "error"}).into()); - } - - // check if auth exists - let details = match db_get_user_reset_auth(db, &auth).await { - None => { - return Ok(json!({"result": "error"}).into()); - } - Some(x) => x, - }; - - if ldap_reset_pw(config, &details, &pass).await.is_err() { - return Ok(json!({"result": "error", "error": "ldap error"}).into()); - }; - - Ok(json!({"result": "success", "success": "Password set"}).into()) + save_to_db(db, &user_details, &auth).await?; } - pub async fn db_get_user(pool: &Pool, user_in: &Option, mail_in: &Option) -> Option { - let user = match user_in { - None => "", - Some(x) => x, - }; - let mail = match mail_in { - None => "", - Some(x) => x, - }; + Ok(json!({"result": "success"}).into()) + } - if let Ok(res) = sqlx::query_as::<_, Accounts>( - r#" + #[derive(Debug, Deserialize)] + pub struct PassResetAuth { + auth: String, + pass: String, + } + + pub async fn auth(mut req: Request) -> tide::Result { + let PassResetAuth { + auth, + pass, + } = req.body_json().await?; + + let config = &req.state().config; + let db = &req.state().db; + + if db_pending_clear_expired(db).await.is_err() { + return Ok(json!({"result": "error"}).into()); + } + + // check if auth exists + let details = match db_get_user_reset_auth(db, &auth).await { + None => { + return Ok(json!({"result": "error"}).into()); + } + Some(x) => x, + }; + + if ldap_reset_pw(config, &details, &pass).await.is_err() { + return Ok(json!({"result": "error", "error": "ldap error"}).into()); + }; + + Ok(json!({"result": "success", "success": "Password set"}).into()) + } + + pub async fn db_get_user(pool: &Pool, user_in: &Option, mail_in: &Option) -> Option { + let user = match user_in { + None => "", + Some(x) => x, + }; + let mail = match mail_in { + None => "", + Some(x) => x, + }; + + if let Ok(res) = sqlx::query_as::<_, Accounts>( + r#" SELECT * FROM accounts WHERE user == ? OR mail ==? "#, - ) - .bind(user) - .bind(mail) - .fetch_all(pool) - .await - { - if !res.is_empty() { - return Some(res[0].to_owned()); - } - } - - None + ) + .bind(user) + .bind(mail) + .fetch_all(pool) + .await + { + if !res.is_empty() { + return Some(res[0].to_owned()); + } } - async fn db_pending_clear_expired(pool: &Pool) -> Result, sqlx::Error> { - sqlx::query_as::<_, AccountsReset>( - r#" + None + } + + async fn db_pending_clear_expired(pool: &Pool) -> Result, sqlx::Error> { + sqlx::query_as::<_, AccountsReset>( + r#" DELETE FROM accounts_reset WHERE date_expiry < ? "#, - ) - .bind(get_now_iso(false)) - .fetch_all(pool) - .await - } + ) + .bind(get_now_iso(false)) + .fetch_all(pool) + .await + } - async fn db_get_user_reset(pool: &Pool, user: &str) -> Option { - if let Ok(res) = sqlx::query_as::<_, AccountsReset>( - r#" + async fn db_get_user_reset(pool: &Pool, user: &str) -> Option { + if let Ok(res) = sqlx::query_as::<_, AccountsReset>( + r#" SELECT * FROM accounts_reset WHERE user == ? "#, - ) - .bind(user) - .fetch_all(pool) - .await - { - if !res.is_empty() { - return Some(res[0].to_owned()); - } - } - - None + ) + .bind(user) + .fetch_all(pool) + .await + { + if !res.is_empty() { + return Some(res[0].to_owned()); + } } - async fn db_get_user_reset_auth(pool: &Pool, auth: &str) -> Option { - if let Ok(res) = sqlx::query_as::<_, AccountsReset>( - r#" + None + } + + async fn db_get_user_reset_auth(pool: &Pool, auth: &str) -> Option { + if let Ok(res) = sqlx::query_as::<_, AccountsReset>( + r#" SELECT * FROM accounts_reset WHERE auth == ? "#, - ) - .bind(auth) - .fetch_all(pool) - .await - { - if !res.is_empty() { - return Some(res[0].to_owned()); + ) + .bind(auth) + .fetch_all(pool) + .await + { + if !res.is_empty() { + return Some(res[0].to_owned()); + } + } + + None + } + + async fn ldap_reset_pw(config: &Config, details: &AccountsReset, 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 = uid_to_dn(&details.user); + + // if so then set password + let tmp = PasswordModify { + // none as we are staying on the same connection. + user_id: Some(&dn), + old_pass: None, + new_pass: Some(pass), + }; + + ldap.extended(tmp)?.success()?; + ldap.unbind()?; + + Ok(()) + } + + fn send_mail(config: &Config, record: &Accounts, auth: &str) -> Result { + let recipient = &record.user; + let mail = &record.mail; + let url_base = "https://account.skynet.ie"; + let link_new = format!("{url_base}/recovery_pass?auth={auth}"); + 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; }" } } - - None - } - - async fn ldap_reset_pw(config: &Config, details: &AccountsReset, 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 = uid_to_dn(&details.user); - - // if so then set password - let tmp = PasswordModify { - // none as we are staying on the same connection. - user_id: Some(&dn), - old_pass: None, - new_pass: Some(pass), - }; - - ldap.extended(tmp)?.success()?; - ldap.unbind()?; - - Ok(()) - } - - fn send_mail(config: &Config, record: &Accounts, auth: &str) -> Result { - let recipient = &record.user; - let mail = &record.mail; - let url_base = "https://account.skynet.ie"; - let link_new = format!("{url_base}/recovery_pass?auth={auth}"); - 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 { + "Here is your password reset link:" + br; + a href=(link_new) { (link_new) } } - 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 { - "Here is your password reset link:" - br; - a href=(link_new) { (link_new) } - } - p { - "If did not request this please ignore." - } - p { - "UL Computer Society" - br; - "Skynet Team" - br; - a href=(discord) { (discord) } - } + p { + "If did not request this please ignore." } - }; + p { + "UL Computer Society" + br; + "Skynet Team" + br; + a href=(discord) { (discord) } + } + } + }; - let body_text = format!( - r#" + let body_text = format!( + r#" Hi {recipient} Here is your password reset link: @@ -263,130 +263,130 @@ pub mod password { Skynet Team {discord} "# - ); + ); - // Build the message. - let email = Message::builder() - .from(sender.parse().unwrap()) - .to(mail.parse().unwrap()) - .subject("Skynet: Password Reset") - .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"); + // Build the message. + let email = Message::builder() + .from(sender.parse().unwrap()) + .to(mail.parse().unwrap()) + .subject("Skynet: Password Reset") + .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()); + 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(); + // 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) - } + // Send the email + mailer.send(&email) + } - async fn save_to_db(db: &Pool, record: &Accounts, auth: &str) -> Result, sqlx::Error> { - // lets start off a 4 hour timeout on password resets - let offset = Utc::now() + Duration::hours(4); + async fn save_to_db(db: &Pool, record: &Accounts, auth: &str) -> Result, sqlx::Error> { + // lets start off a 4 hour timeout on password resets + let offset = Utc::now() + Duration::hours(4); - sqlx::query_as::<_, AccountsReset>( - " + sqlx::query_as::<_, AccountsReset>( + " INSERT OR REPLACE INTO accounts_reset (user, auth_code, date_expiry) VALUES (?1, ?2, ?3) ", - ) - .bind(record.user.to_owned()) - .bind(auth.to_owned()) - .bind(offset.to_rfc3339_opts(SecondsFormat::Millis, true)) - .fetch_optional(db) - .await - } + ) + .bind(record.user.to_owned()) + .bind(auth.to_owned()) + .bind(offset.to_rfc3339_opts(SecondsFormat::Millis, true)) + .fetch_optional(db) + .await + } } pub mod username { - use super::password::db_get_user; - use super::*; + use super::password::db_get_user; + use super::*; - // far simpler, accept email, send notification via email + // far simpler, accept email, send notification via email - #[derive(Debug, Deserialize)] - struct UsernameReminder { - email: String, + #[derive(Debug, Deserialize)] + struct UsernameReminder { + email: String, + } + + pub async fn submit(mut req: Request) -> tide::Result { + let UsernameReminder { + email, + } = req.body_json().await?; + + // check that any mail is not using @skynet.ie + + if email.trim().ends_with("@skynet.ie") { + // all responses from this are a success + return Ok(json!({"result": "error", "error": "Skynet email not permitted."}).into()); } - pub async fn submit(mut req: Request) -> tide::Result { - let UsernameReminder { - email, - } = req.body_json().await?; + let config = &req.state().config; + let db = &req.state().db; - // check that any mail is not using @skynet.ie + // considering the local db is updated hourly (or less) use that instead of teh ldap for lookups + let user_details = match db_get_user(db, &None, &Some(email)).await { + None => { + return Ok(json!({"result": "success"}).into()); + } + Some(x) => x, + }; - if email.trim().ends_with("@skynet.ie") { - // all responses from this are a success - return Ok(json!({"result": "error", "error": "Skynet email not permitted."}).into()); - } - - let config = &req.state().config; - let db = &req.state().db; - - // considering the local db is updated hourly (or less) use that instead of teh ldap for lookups - let user_details = match db_get_user(db, &None, &Some(email)).await { - None => { - return Ok(json!({"result": "success"}).into()); - } - Some(x) => x, - }; - - // user does not have a different email address set - if user_details.mail.trim().ends_with("@skynet.ie") { - // not returning an error here as there is no need to let the person requesting what email the user has - return Ok(json!({"result": "success"}).into()); - } - - send_mail(config, &user_details).ok(); - - Ok(json!({"result": "success"}).into()) + // user does not have a different email address set + if user_details.mail.trim().ends_with("@skynet.ie") { + // not returning an error here as there is no need to let the person requesting what email the user has + return Ok(json!({"result": "success"}).into()); } - fn send_mail(config: &Config, record: &Accounts) -> Result { - let recipient = &record.user; - let mail = &record.mail; - let discord = "https://discord.skynet.ie"; - let sender = format!("UL Computer Society <{}>", &config.mail_user); + send_mail(config, &user_details).ok(); - // 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 there," } - p { - "You requested a username reminder: " (recipient) - } - p { - "If did not request this please ignore." - } - p { - "UL Computer Society" - br; - "Skynet Team" - br; - a href=(discord) { (discord) } - } - } - }; + Ok(json!({"result": "success"}).into()) + } - let body_text = format!( - r#" + fn send_mail(config: &Config, record: &Accounts) -> Result { + let recipient = &record.user; + let mail = &record.mail; + 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 there," } + p { + "You requested a username reminder: " (recipient) + } + p { + "If did not request this please ignore." + } + p { + "UL Computer Society" + br; + "Skynet Team" + br; + a href=(discord) { (discord) } + } + } + }; + + let body_text = format!( + r#" Hi there, You requested a username reminder: {recipient} @@ -397,207 +397,207 @@ pub mod username { Skynet Team {discord} "# - ); + ); - // Build the message. - let email = Message::builder() - .from(sender.parse().unwrap()) - .to(mail.parse().unwrap()) - .subject("Skynet: Username Reminder") - .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"); + // Build the message. + let email = Message::builder() + .from(sender.parse().unwrap()) + .to(mail.parse().unwrap()) + .subject("Skynet: Username Reminder") + .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()); + 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(); + // 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) - } + // Send the email + mailer.send(&email) + } } pub mod ssh { - use super::*; - use crate::AccountsSSH; - use ldap3::Mod; - use ssh_key::{AuthorizedKeys, SshSig}; - use std::collections::HashSet; - use std::fs; + use super::*; + use crate::AccountsSSH; + use ldap3::Mod; + use ssh_key::{AuthorizedKeys, SshSig}; + use std::collections::HashSet; + use std::fs; - // this is for a legacy member who has forgotten their account password to be able to set an email. - // With an email they can do a password recovery + // this is for a legacy member who has forgotten their account password to be able to set an email. + // With an email they can do a password recovery - #[derive(Debug, Deserialize)] - struct RequestChallenge { - user: String, - email: String, + #[derive(Debug, Deserialize)] + struct RequestChallenge { + user: String, + email: String, + } + + pub async fn request(mut req: Request) -> tide::Result { + let RequestChallenge { + user, + email, + } = req.body_json().await?; + + // check that any mail is not using @skynet.ie + if email.trim().ends_with("@skynet.ie") { + // all responses from this are a success + return Ok(json!({"result": "error", "error": "Skynet email not permitted."}).into()); } - pub async fn request(mut req: Request) -> tide::Result { - let RequestChallenge { - user, - email, - } = req.body_json().await?; + let config = &req.state().config; - // check that any mail is not using @skynet.ie - if email.trim().ends_with("@skynet.ie") { - // all responses from this are a success - return Ok(json!({"result": "error", "error": "Skynet email not permitted."}).into()); + // check if //.ssh/authorized_keys exists + let root = &config.ssh_root; + let path = format!("{}/{}/.ssh/authorized_keys", root, user); + let mut keys = vec![]; + if fs::read_to_string(&path).is_ok() { + if let Ok(x) = AuthorizedKeys::read_file(path) { + for entry in x { + if let Ok(y) = entry.public_key().to_openssh() { + keys.push(y); + } } + } + } - let config = &req.state().config; + if keys.is_empty() { + return Ok(json!({ "result": "success", "success": { "auth": "", "keys": keys }}).into()); + } - // check if //.ssh/authorized_keys exists - let root = &config.ssh_root; - let path = format!("{}/{}/.ssh/authorized_keys", root, user); - let mut keys = vec![]; - if fs::read_to_string(&path).is_ok() { - if let Ok(x) = AuthorizedKeys::read_file(path) { - for entry in x { - if let Ok(y) = entry.public_key().to_openssh() { - keys.push(y); - } - } - } - } + let db = &req.state().db; - if keys.is_empty() { - return Ok(json!({ "result": "success", "success": { "auth": "", "keys": keys }}).into()); - } - - let db = &req.state().db; - - // check if there is ane listing entry, use that auth if exists - if let Ok(result) = sqlx::query_as::<_, AccountsSSH>( - r#" + // check if there is ane listing entry, use that auth if exists + if let Ok(result) = sqlx::query_as::<_, AccountsSSH>( + r#" SELECT * FROM accounts_ssh WHERE user == ? "#, - ) - .bind(&user) - .fetch_one(db) - .await - { - return Ok(json!({ "result": "success", "success": { "auth": result.auth_code, "keys": keys }}).into()); - } + ) + .bind(&user) + .fetch_one(db) + .await + { + return Ok(json!({ "result": "success", "success": { "auth": result.auth_code, "keys": keys }}).into()); + } - // not in db, generate auth and save - let auth = random_string(50); - if sqlx::query_as::<_, AccountsSSH>( - " + // not in db, generate auth and save + let auth = random_string(50); + if sqlx::query_as::<_, AccountsSSH>( + " INSERT OR REPLACE INTO accounts_ssh (user, auth_code, email) VALUES (?1, ?2, ?3) ", - ) - .bind(&user) - .bind(&auth) - .bind(&email) - .fetch_optional(db) - .await - .is_err() - { - // dont return any keys - return Ok(json!({ "result": "success", "success": { "auth": "", "keys": [] }}).into()); - } - - // return the full thing - Ok(json!({"result": "success","success": { "auth": auth,"keys": keys }}).into()) + ) + .bind(&user) + .bind(&auth) + .bind(&email) + .fetch_optional(db) + .await + .is_err() + { + // dont return any keys + return Ok(json!({ "result": "success", "success": { "auth": "", "keys": [] }}).into()); } - #[derive(Debug, Deserialize)] - struct RequestVerify { - user: String, - auth_signed: String, - } + // return the full thing + Ok(json!({"result": "success","success": { "auth": auth,"keys": keys }}).into()) + } - // echo "auth code" | ssh-keygen -Y sign -n file -f /path/to/key - pub async fn verify(mut req: Request) -> tide::Result { - let RequestVerify { - user, - auth_signed, - } = req.body_json().await?; + #[derive(Debug, Deserialize)] + struct RequestVerify { + user: String, + auth_signed: String, + } - let db = &req.state().db; - let config = &req.state().config; - let details = if let Ok(result) = sqlx::query_as::<_, AccountsSSH>( - r#" + // echo "auth code" | ssh-keygen -Y sign -n file -f /path/to/key + pub async fn verify(mut req: Request) -> tide::Result { + let RequestVerify { + user, + auth_signed, + } = req.body_json().await?; + + let db = &req.state().db; + let config = &req.state().config; + let details = if let Ok(result) = sqlx::query_as::<_, AccountsSSH>( + r#" SELECT * FROM accounts_ssh WHERE user == ? "#, - ) - .bind(&user) - .fetch_one(db) - .await - { - result - } else { - return Ok(json!({ "result": "error"}).into()); - }; + ) + .bind(&user) + .fetch_one(db) + .await + { + result + } else { + return Ok(json!({ "result": "error"}).into()); + }; - // check if //.ssh/authorized_keys exists - //let root = "/skynet_old"; - let root = "."; - let path = format!("{}/{}/.ssh/authorized_keys", root, user); + // check if //.ssh/authorized_keys exists + //let root = "/skynet_old"; + let root = "."; + let path = format!("{}/{}/.ssh/authorized_keys", root, user); - let sig = match SshSig::from_pem(auth_signed) { - Ok(x) => x, - Err(_) => { - return Ok(json!({ "result": "error", "error": "Incorrect signed format"}).into()); - } - }; + let sig = match SshSig::from_pem(auth_signed) { + Ok(x) => x, + Err(_) => { + return Ok(json!({ "result": "error", "error": "Incorrect signed format"}).into()); + } + }; - // when ye echo it adds a newline to the end of the "file", need to duplicate it here. - let msg_tmp = format!("{}\n", details.auth_code); - let msg = msg_tmp.as_bytes(); + // when ye echo it adds a newline to the end of the "file", need to duplicate it here. + let msg_tmp = format!("{}\n", details.auth_code); + let msg = msg_tmp.as_bytes(); - let mut valid = false; - if fs::read_to_string(&path).is_ok() { - if let Ok(x) = AuthorizedKeys::read_file(path) { - for entry in x { - let key = entry.public_key(); - if key.verify("file", msg, &sig).is_ok() { - valid = true; - break; - } - } - } + let mut valid = false; + if fs::read_to_string(&path).is_ok() { + if let Ok(x) = AuthorizedKeys::read_file(path) { + for entry in x { + let key = entry.public_key(); + if key.verify("file", msg, &sig).is_ok() { + valid = true; + break; + } } + } + } - if !valid { - return Ok(json!({"result": "error", "error": "no valid key"}).into()); - } + if !valid { + return Ok(json!({"result": "error", "error": "no valid key"}).into()); + } - // add to ldap - let mut ldap = LdapConn::new(&config.ldap_host)?; - ldap.simple_bind(&config.ldap_admin, &config.ldap_admin_pw)?.success()?; + // add to ldap + let mut ldap = LdapConn::new(&config.ldap_host)?; + ldap.simple_bind(&config.ldap_admin, &config.ldap_admin_pw)?.success()?; - let mods = vec![Mod::Replace(String::from("mail"), HashSet::from([details.email]))]; + let mods = vec![Mod::Replace(String::from("mail"), HashSet::from([details.email]))]; - let dn = format!("uid={},ou=users,dc=skynet,dc=ie", &user); - ldap.modify(&dn, mods)?.success()?; - ldap.unbind()?; + let dn = format!("uid={},ou=users,dc=skynet,dc=ie", &user); + ldap.modify(&dn, mods)?.success()?; + ldap.unbind()?; - // delete from tmp - sqlx::query_as::<_, AccountsSSH>( - r#" + // delete from tmp + sqlx::query_as::<_, AccountsSSH>( + r#" DELETE FROM accounts_ssh WHERE user == ? "#, - ) - .bind(&user) - .fetch_optional(db) - .await?; + ) + .bind(&user) + .fetch_optional(db) + .await?; - // return the full thing - Ok(json!({"result": "success", "success": "key valid"}).into()) - } + // return the full thing + Ok(json!({"result": "success", "success": "key valid"}).into()) + } } diff --git a/src/methods/account_update.rs b/src/methods/account_update.rs index 1e9a144..a0c2f56 100644 --- a/src/methods/account_update.rs +++ b/src/methods/account_update.rs @@ -3,152 +3,152 @@ use ldap3::{exop::PasswordModify, LdapConn, Mod, Scope, SearchEntry}; use sqlx::{Pool, Sqlite}; use std::collections::{HashMap, HashSet}; use tide::{ - prelude::{json, Deserialize, Serialize}, - Request, + prelude::{json, Deserialize, Serialize}, + Request, }; #[derive(Debug, Deserialize)] pub struct LdapUpdate { - user: String, - pass: String, - field: String, - value: String, + user: String, + pass: String, + field: String, + value: String, } #[derive(Debug, Serialize)] pub struct ModifyResult { - mail: Option, - #[serde(rename = "sshPublicKey")] - ssh_public_key: Option, - cn: Option, - #[serde(rename = "skDiscord")] - sk_discord: Option, + mail: Option, + #[serde(rename = "sshPublicKey")] + ssh_public_key: Option, + cn: Option, + #[serde(rename = "skDiscord")] + sk_discord: Option, } /// Handles updating a single field with the users own password pub async fn submit(mut req: Request) -> tide::Result { - let LdapUpdate { - user, - pass, - field, - value, - } = req.body_json().await?; + let LdapUpdate { + user, + pass, + field, + value, + } = req.body_json().await?; - // check that any mail is not using @skynet.ie - if field == "mail" && value.trim().ends_with("@skynet.ie") { - return Ok(json!({"result": "error", "error": "Skynet email not valid contact address"}).into()); + // check that any mail is not using @skynet.ie + if field == "mail" && value.trim().ends_with("@skynet.ie") { + return Ok(json!({"result": "error", "error": "Skynet email not valid contact address"}).into()); + } + + let config = &req.state().config; + let db = &req.state().db; + + // easier to give each request its own connection + let mut ldap = LdapConn::new(&config.ldap_host)?; + + let dn = format!("uid={},ou=users,dc=skynet,dc=ie", user); + ldap.simple_bind(&dn, &pass)?.success()?; + + // always assume insecure + let mut pw_keep_same = false; + let mut is_skynet_user = false; + + // get the users current password hash + let (rs, _res) = ldap.search(&dn, Scope::Base, "(objectClass=*)", vec!["userPassword", "memberOf"])?.success()?; + if !rs.is_empty() { + let tmp = SearchEntry::construct(rs[0].clone()); + if tmp.attrs.contains_key("userPassword") && !tmp.attrs["userPassword"].is_empty() && tmp.attrs["userPassword"][0].starts_with("{SSHA512}") { + pw_keep_same = true; + } + if tmp.attrs.contains_key("memberOf") { + for group in tmp.attrs["memberOf"].clone() { + if group.contains("skynet-users") { + is_skynet_user = true; + } + } + } + } + + // check if the password field itself is being updated + let pass_new = if &field != "userPassword" { + if !is_skynet_user && &field == "mail" { + activate_group(db, config, &user, &value).await; } - let config = &req.state().config; - let db = &req.state().db; + // if password is not being updated then just update the required field + let mods = vec![ + // the value we are updating + Mod::Replace(field, HashSet::from([value])), + ]; - // easier to give each request its own connection - let mut ldap = LdapConn::new(&config.ldap_host)?; + ldap.modify(&dn, mods)?.success()?; - let dn = format!("uid={},ou=users,dc=skynet,dc=ie", user); - ldap.simple_bind(&dn, &pass)?.success()?; + // pass back the "old" and "new" passwords + // using this means we can create teh vars without them needing to be mutable + pass.clone() + } else { + // password is going to be updated, even if the old value is not starting with "{SSHA512}" + pw_keep_same = false; + value + }; - // always assume insecure - let mut pw_keep_same = false; - let mut is_skynet_user = false; - - // get the users current password hash - let (rs, _res) = ldap.search(&dn, Scope::Base, "(objectClass=*)", vec!["userPassword", "memberOf"])?.success()?; - if !rs.is_empty() { - let tmp = SearchEntry::construct(rs[0].clone()); - if tmp.attrs.contains_key("userPassword") && !tmp.attrs["userPassword"].is_empty() && tmp.attrs["userPassword"][0].starts_with("{SSHA512}") { - pw_keep_same = true; - } - if tmp.attrs.contains_key("memberOf") { - for group in tmp.attrs["memberOf"].clone() { - if group.contains("skynet-users") { - is_skynet_user = true; - } - } - } - } - - // check if the password field itself is being updated - let pass_new = if &field != "userPassword" { - if !is_skynet_user && &field == "mail" { - activate_group(db, config, &user, &value).await; - } - - // if password is not being updated then just update the required field - let mods = vec![ - // the value we are updating - Mod::Replace(field, HashSet::from([value])), - ]; - - ldap.modify(&dn, mods)?.success()?; - - // pass back the "old" and "new" passwords - // using this means we can create teh vars without them needing to be mutable - pass.clone() - } else { - // password is going to be updated, even if the old value is not starting with "{SSHA512}" - pw_keep_same = false; - value + // changing teh password because of an explicit request or upgrading teh security. + if !pw_keep_same { + // really easy to update password once ye know how + let tmp = PasswordModify { + // none as we are staying on the same connection + user_id: None, + old_pass: Some(&pass), + new_pass: Some(&pass_new), }; - // changing teh password because of an explicit request or upgrading teh security. - if !pw_keep_same { - // really easy to update password once ye know how - let tmp = PasswordModify { - // none as we are staying on the same connection - user_id: None, - old_pass: Some(&pass), - new_pass: Some(&pass_new), - }; + ldap.extended(tmp)?.success()?; + }; - ldap.extended(tmp)?.success()?; - }; + let result = get_result(&mut ldap, &dn); - let result = get_result(&mut ldap, &dn); + ldap.unbind()?; - ldap.unbind()?; - - Ok(json!({"result": "success", "success": result}).into()) + Ok(json!({"result": "success", "success": result}).into()) } fn get_result(ldap: &mut LdapConn, dn: &str) -> ModifyResult { - let mut result = ModifyResult { - mail: None, - ssh_public_key: None, - cn: None, - sk_discord: None, - }; + let mut result = ModifyResult { + mail: None, + ssh_public_key: None, + cn: None, + sk_discord: None, + }; - if let Ok(temp) = ldap.search(dn, Scope::Base, "(objectClass=*)", vec!["mail", "sshPublicKey", "cn", "skDiscord"]) { - if let Ok((rs, _res)) = temp.success() { - if !rs.is_empty() { - let tmp = SearchEntry::construct(rs[0].clone()); - result.mail = get_result_values(&tmp.attrs, "mail"); - result.ssh_public_key = get_result_values(&tmp.attrs, "sshPublicKey"); - result.cn = get_result_values(&tmp.attrs, "cn"); - result.sk_discord = get_result_values(&tmp.attrs, "skDiscord"); - } - } + if let Ok(temp) = ldap.search(dn, Scope::Base, "(objectClass=*)", vec!["mail", "sshPublicKey", "cn", "skDiscord"]) { + if let Ok((rs, _res)) = temp.success() { + if !rs.is_empty() { + let tmp = SearchEntry::construct(rs[0].clone()); + result.mail = get_result_values(&tmp.attrs, "mail"); + result.ssh_public_key = get_result_values(&tmp.attrs, "sshPublicKey"); + result.cn = get_result_values(&tmp.attrs, "cn"); + result.sk_discord = get_result_values(&tmp.attrs, "skDiscord"); + } } + } - result + result } fn get_result_values(attrs: &HashMap>, field: &str) -> Option { - if let Some(field) = attrs.get(field) { - if !field.is_empty() { - return Some(field[0].clone()); - } + if let Some(field) = attrs.get(field) { + if !field.is_empty() { + return Some(field[0].clone()); } - None + } + None } async fn activate_group(db: &Pool, config: &Config, user: &str, mail: &str) { - // check if user has this mail in teh wolves db - if !get_wolves_mail(db, mail).await.is_empty() { - // if so then activate - if let Err(e) = update_group(config, "skynet-users", &vec![user.to_string()], false).await { - println!("Couldnt add {} to skynet-users: {:?}", user, e) - } + // check if user has this mail in teh wolves db + if !get_wolves_mail(db, mail).await.is_empty() { + // if so then activate + if let Err(e) = update_group(config, "skynet-users", &vec![user.to_string()], false).await { + println!("Couldnt add {} to skynet-users: {:?}", user, e) } + } }