diff --git a/.gitignore b/.gitignore index 55d1409..ad64ae3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +/.cargo /.idea .env *.env @@ -10,4 +11,4 @@ result tmp/ tmp.* -*.csv \ No newline at end of file +*.csv diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..8763feb --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable-2023-12-07" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 5e840f4..fcbc117 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ pub mod methods; use chrono::{Datelike, SecondsFormat, Utc}; use dotenvy::dotenv; -use ldap3::{LdapConn, Mod}; +use ldap3::{exop::PasswordModify, LdapConn, Mod, Scope, SearchEntry}; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use sqlx::{ sqlite::{SqliteConnectOptions, SqlitePoolOptions}, @@ -321,3 +321,104 @@ pub async fn update_group(config: &Config, group: &str, users: &Vec, rep Ok(()) } + +#[derive(Debug, Deserialize)] +pub struct LdapAuth { + user: String, + pass: String, +} + +#[derive(Debug)] +pub struct LdapAuthResult { + ldap: LdapConn, + dn: String, + is_skynet_user: bool, +} + +/// Auths and updates the users password hash +pub async fn auth_user(auth: &LdapAuth, config: &Config) -> Option { + let LdapAuth { + user, + pass, + } = auth; + + // easier to give each request its own connection + let mut ldap = match LdapConn::new(&config.ldap_host) { + Ok(x) => x, + Err(err) => { + println!("{:?}", err); + return None; + } + }; + + let dn = format!("uid={},ou=users,dc=skynet,dc=ie", user); + + // authenticate with the users own apssword + match ldap.simple_bind(&dn, pass) { + Ok(result) => match result.success() { + Ok(_) => {} + Err(err) => { + println!("{:?}", err); + return None; + } + }, + Err(err) => { + println!("{:?}", err); + return None; + } + } + + // always assume insecure + let mut pw_keep_same = false; + let mut is_skynet_user = false; + + // get the users current password hash + if let Ok(result) = ldap.search(&dn, Scope::Base, "(objectClass=*)", vec!["userPassword"]) { + if let Ok((rs, _res)) = result.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; + } + } + } + } + } + } + + if !pw_keep_same { + let tmp = PasswordModify { + // No need to set the id, since we are already authed for this + user_id: None, + old_pass: Some(pass), + // although the same as the old it will allow it to be re-hashed to SSHA512 + new_pass: Some(pass), + }; + + match ldap.extended(tmp) { + Ok(x) => { + match x.success() { + Ok(_) => {} + Err(err) => { + println!("{:?}", err); + // not returning None as the user still managed to auth + } + } + } + Err(err) => { + println!("{:?}", err); + } + } + } + + Some(LdapAuthResult { + ldap, + dn, + is_skynet_user, + }) +} diff --git a/src/methods/account_update.rs b/src/methods/account_update.rs index 199d2f7..6c95903 100644 --- a/src/methods/account_update.rs +++ b/src/methods/account_update.rs @@ -1,7 +1,7 @@ -use crate::{methods::account_new::email::get_wolves_mail, update_group, Accounts, Config, State}; -use ldap3::{exop::PasswordModify, LdapConn, Mod, Scope, SearchEntry}; +use crate::{methods::account_new::email::get_wolves_mail, update_group, Accounts, Config, LdapAuth, LdapAuthResult, State}; +use ldap3::{exop::PasswordModify, Mod}; use sqlx::{Pool, Sqlite}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use tide::{ prelude::{json, Deserialize, Serialize}, Request, @@ -9,8 +9,7 @@ use tide::{ #[derive(Debug, Deserialize)] pub struct LdapUpdate { - user: String, - pass: String, + auth: LdapAuth, field: String, value: String, } @@ -26,8 +25,7 @@ pub struct ModifyResult { /// Handles updating a single field with the users own password pub async fn submit(mut req: Request) -> tide::Result { let LdapUpdate { - user, - pass, + auth, field, value, } = req.body_json().await?; @@ -41,35 +39,19 @@ pub async fn submit(mut req: Request) -> tide::Result { 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; - } - } - } - } + let LdapAuthResult { + mut ldap, + dn, + is_skynet_user, + } = match crate::auth_user(&auth, config).await { + None => return Ok(json!({"result": "error", "error": "Failed to authenticate"}).into()), + Some(x) => x, + }; // check if the password field itself is being updated - let pass_new = if &field != "userPassword" { + if &field != "userPassword" { if !is_skynet_user && &field == "mail" { - activate_group(db, config, &user, &value).await; + activate_group(db, config, &auth.user, &value).await; } // if password is not being updated then just update the required field @@ -79,69 +61,26 @@ pub async fn submit(mut req: Request) -> tide::Result { ]; 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.clone() - }; - - // 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), + old_pass: Some(&auth.pass), + new_pass: Some(&value), }; ldap.extended(tmp)?.success()?; }; - let result = get_result(&mut ldap, &dn); - ldap.unbind()?; // if its mail update the local db + // here in case it fails above if &field == "mail" { update_local_db(db, "mail", &value).await.ok(); } - 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, - }; - - 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 -} - -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()); - } - } - None + Ok(json!({"result": "success"}).into()) } async fn activate_group(db: &Pool, config: &Config, user: &str, mail: &str) {