Merge branch '#25-split-out-auth-code' into 'main'
feat: split out the auth mechanism for a user's account See merge request compsoc1/skynet/ldap/backend!19
This commit is contained in:
commit
7ff179f5f2
2 changed files with 121 additions and 81 deletions
103
src/lib.rs
103
src/lib.rs
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue