feat: split out the auth mechanism for a user's account #46

Merged
silver merged 1 commit from #25-split-out-auth-code into main 2023-12-27 17:04:49 +00:00
2 changed files with 121 additions and 81 deletions
Showing only changes of commit 8f4f4b11d8 - Show all commits

View file

@ -2,7 +2,7 @@ pub mod methods;
use chrono::{Datelike, SecondsFormat, Utc}; use chrono::{Datelike, SecondsFormat, Utc};
use dotenvy::dotenv; use dotenvy::dotenv;
use ldap3::{LdapConn, Mod}; use ldap3::{exop::PasswordModify, LdapConn, Mod, Scope, SearchEntry};
use rand::{distributions::Alphanumeric, thread_rng, Rng}; use rand::{distributions::Alphanumeric, thread_rng, Rng};
use sqlx::{ use sqlx::{
sqlite::{SqliteConnectOptions, SqlitePoolOptions}, sqlite::{SqliteConnectOptions, SqlitePoolOptions},
@ -321,3 +321,104 @@ pub async fn update_group(config: &Config, group: &str, users: &Vec<String>, rep
Ok(()) 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<LdapAuthResult> {
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,
})
}

View file

@ -1,7 +1,7 @@
use crate::{methods::account_new::email::get_wolves_mail, update_group, Accounts, Config, State}; use crate::{methods::account_new::email::get_wolves_mail, update_group, Accounts, Config, LdapAuth, LdapAuthResult, State};
use ldap3::{exop::PasswordModify, LdapConn, Mod, Scope, SearchEntry}; use ldap3::{exop::PasswordModify, Mod};
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use std::collections::{HashMap, HashSet}; use std::collections::HashSet;
use tide::{ use tide::{
prelude::{json, Deserialize, Serialize}, prelude::{json, Deserialize, Serialize},
Request, Request,
@ -9,8 +9,7 @@ use tide::{
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct LdapUpdate { pub struct LdapUpdate {
user: String, auth: LdapAuth,
pass: String,
field: String, field: String,
value: String, value: String,
} }
@ -26,8 +25,7 @@ pub struct ModifyResult {
/// Handles updating a single field with the users own password /// Handles updating a single field with the users own password
pub async fn submit(mut req: Request<State>) -> tide::Result { pub async fn submit(mut req: Request<State>) -> tide::Result {
let LdapUpdate { let LdapUpdate {
user, auth,
pass,
field, field,
value, value,
} = req.body_json().await?; } = req.body_json().await?;
@ -41,35 +39,19 @@ pub async fn submit(mut req: Request<State>) -> tide::Result {
let db = &req.state().db; let db = &req.state().db;
// easier to give each request its own connection // easier to give each request its own connection
let mut ldap = LdapConn::new(&config.ldap_host)?; let LdapAuthResult {
mut ldap,
let dn = format!("uid={},ou=users,dc=skynet,dc=ie", user); dn,
ldap.simple_bind(&dn, &pass)?.success()?; is_skynet_user,
} = match crate::auth_user(&auth, config).await {
// always assume insecure None => return Ok(json!({"result": "error", "error": "Failed to authenticate"}).into()),
let mut pw_keep_same = false; Some(x) => x,
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 // check if the password field itself is being updated
let pass_new = if &field != "userPassword" { if &field != "userPassword" {
if !is_skynet_user && &field == "mail" { 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 // if password is not being updated then just update the required field
@ -79,69 +61,26 @@ pub async fn submit(mut req: Request<State>) -> tide::Result {
]; ];
ldap.modify(&dn, mods)?.success()?; 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 { } 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 { let tmp = PasswordModify {
// none as we are staying on the same connection // none as we are staying on the same connection
user_id: None, user_id: None,
old_pass: Some(&pass), old_pass: Some(&auth.pass),
new_pass: Some(&pass_new), new_pass: Some(&value),
}; };
ldap.extended(tmp)?.success()?; ldap.extended(tmp)?.success()?;
}; };
let result = get_result(&mut ldap, &dn);
ldap.unbind()?; ldap.unbind()?;
// if its mail update the local db // if its mail update the local db
// here in case it fails above
if &field == "mail" { if &field == "mail" {
update_local_db(db, "mail", &value).await.ok(); update_local_db(db, "mail", &value).await.ok();
} }
Ok(json!({"result": "success", "success": result}).into()) Ok(json!({"result": "success"}).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<String, Vec<String>>, field: &str) -> Option<String> {
if let Some(field) = attrs.get(field) {
if !field.is_empty() {
return Some(field[0].clone());
}
}
None
} }
async fn activate_group(db: &Pool<Sqlite>, config: &Config, user: &str, mail: &str) { async fn activate_group(db: &Pool<Sqlite>, config: &Config, user: &str, mail: &str) {