From 11b348326a053b7b8798ca8aa08cb709df534263 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 21:20:20 +0100 Subject: [PATCH 1/6] fix: no need for skSecure --- src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2bd7bea..fe7831e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -181,7 +181,7 @@ async fn update_accounts(pool: &Pool, config: &Config) { 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", "skSecure"]) { + 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); @@ -215,8 +215,8 @@ async fn update_accounts(pool: &Pool, config: &Config) { 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("skSecure") && !tmp.attrs["skSecure"].is_empty() { - tmp_account.secure = 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() { From bf1d91e110a40f854e4c51a3b560cb4d38831b8c Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 21:39:32 +0100 Subject: [PATCH 2/6] feat: reduce complexity around skSecure #5 --- src/methods/account_update.rs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/methods/account_update.rs b/src/methods/account_update.rs index 628f5ff..db2b19d 100644 --- a/src/methods/account_update.rs +++ b/src/methods/account_update.rs @@ -38,7 +38,6 @@ pub async fn post_update_ldap(mut req: Request) -> tide::Result { // always assume insecure let mut pw_keep_same = false; - let mut pw_secure = false; // get the users current password hash let (rs, _res) = ldap.search(&dn, Scope::Base, "(objectClass=*)", vec!["userPassword"])?.success()?; @@ -46,44 +45,34 @@ pub async fn post_update_ldap(mut req: Request) -> tide::Result { let tmp = SearchEntry::construct(rs[0].clone()); if !tmp.attrs["userPassword"].is_empty() && tmp.attrs["userPassword"][0].starts_with("{SSHA512}") { pw_keep_same = true; - pw_secure = true; - } - if tmp.attrs.contains_key("skSecure") && !tmp.attrs["skSecure"].is_empty() && tmp.attrs["skSecure"][0] == "1" { - pw_secure = true; } } // check if the password field itself is being updated let (pass_old, pass_new) = if &field != "userPassword" { // if password is not being updated then just update the required field - let mut mods = vec![ - // main value we are updating + let mods = vec![ + // the value we are updating Mod::Replace(field, HashSet::from([value])), ]; - // if teh password is changing then its inherentrly secure, same if its currently an empty field - if !pw_keep_same || !pw_secure { - mods.push(Mod::Replace(String::from("skSecure"), HashSet::from([String::from("1")]))); - } - 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(), pass.clone()) } else { // password is going to be updated, even if the old value is not starting with "{SSHA512}" - pw_keep_same = false; (pass.clone(), 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_old), new_pass: Some(&pass_new), }; From 344d958aec4ba2750713f76e63d310e83e040584 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 22:20:15 +0100 Subject: [PATCH 3/6] feat: further reduction in complexity #5 --- src/methods/account_update.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/methods/account_update.rs b/src/methods/account_update.rs index db2b19d..9dd0f27 100644 --- a/src/methods/account_update.rs +++ b/src/methods/account_update.rs @@ -49,7 +49,7 @@ pub async fn post_update_ldap(mut req: Request) -> tide::Result { } // check if the password field itself is being updated - let (pass_old, pass_new) = if &field != "userPassword" { + let pass_new = if &field != "userPassword" { // if password is not being updated then just update the required field let mods = vec![ // the value we are updating @@ -60,11 +60,11 @@ pub async fn post_update_ldap(mut req: Request) -> tide::Result { // pass back the "old" and "new" passwords // using this means we can create teh vars without them needing to be mutable - (pass.clone(), pass.clone()) + pass.clone() } else { // password is going to be updated, even if the old value is not starting with "{SSHA512}" pw_keep_same = false; - (pass.clone(), value) + value }; // changing teh password because of an explicit request or upgrading teh security. @@ -73,7 +73,7 @@ pub async fn post_update_ldap(mut req: Request) -> tide::Result { let tmp = PasswordModify { // none as we are staying on the same connection user_id: None, - old_pass: Some(&pass_old), + old_pass: Some(&pass), new_pass: Some(&pass_new), }; From bfee0c519b3cddbb41a9a58083919d3cf3217f18 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Wed, 2 Aug 2023 13:51:14 +0100 Subject: [PATCH 4/6] fix: all committee members should be properly updated now --- src/bin/update_groups.rs | 57 ++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/src/bin/update_groups.rs b/src/bin/update_groups.rs index e1c4e9d..8ea1d30 100644 --- a/src/bin/update_groups.rs +++ b/src/bin/update_groups.rs @@ -8,14 +8,14 @@ use std::{collections::HashSet, env, error::Error}; async fn main() -> tide::Result<()> { let config = get_config(); - update_users(&config).await?; - update_admin(&config).await?; - update_committee(&config).await?; + update(&config).await?; Ok(()) } -async fn update_users(config: &Config) -> tide::Result<()> { +async fn update(config: &Config) -> tide::Result<()> { + dotenv().ok(); + let mut users_tmp = HashSet::new(); // default user to ensure group is never empty users_tmp.insert(String::from("compsoc")); @@ -31,6 +31,27 @@ async fn update_users(config: &Config) -> tide::Result<()> { users_tmp.insert(user); } + if let Ok(x) = env::var("USERS_ADMIN") { + let users = x.split(',').collect::>(); + + update_group(config, "skynet-admins", &users, true).await?; + // admins automatically get added as users + for user in users { + users_tmp.insert(user.to_string()); + } + } + + // read from teh env + if let Ok(x) = env::var("USERS_COMMITTEE") { + let users = x.split(',').collect::>(); + + update_group(config, "skynet-committee", &users, true).await?; + // committee automatically get added as users + for user in users { + 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::>() { @@ -50,34 +71,6 @@ fn uid_to_dn(uid: &str) -> String { format!("uid={},ou=users,dc=skynet,dc=ie", uid) } -async fn update_admin(config: &Config) -> tide::Result<()> { - dotenv().ok(); - - // read from teh env - if let Ok(x) = env::var("USERS_ADMIN") { - let users = x.split(',').collect::>(); - - update_group(config, "skynet-admins", &users, true).await?; - // admins automatically get added as users - update_group(config, "skynet-users", &users, false).await?; - } - Ok(()) -} - -async fn update_committee(config: &Config) -> tide::Result<()> { - dotenv().ok(); - - // read from teh env - if let Ok(x) = env::var("USERS_COMMITTEE") { - let users = x.split(',').collect::>(); - - update_group(config, "skynet-committee", &users, true).await?; - // admins automatically get added as users - update_group(config, "skynet-users", &users, false).await?; - } - Ok(()) -} - async fn update_group(config: &Config, group: &str, users: &[&str], replace: bool) -> tide::Result<()> { if users.is_empty() { return Ok(()); From bd18ad759e4dc9ffb5c9fe7b4156e608cbe7cdc3 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Wed, 2 Aug 2023 14:52:58 +0100 Subject: [PATCH 5/6] fix: all users will need to havea secure/updated password to log in Closes #7 --- src/bin/update_groups.rs | 84 +++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 15 deletions(-) diff --git a/src/bin/update_groups.rs b/src/bin/update_groups.rs index 8ea1d30..2676980 100644 --- a/src/bin/update_groups.rs +++ b/src/bin/update_groups.rs @@ -1,4 +1,3 @@ -use dotenvy::dotenv; use ldap3::{LdapConn, Mod}; use skynet_ldap_backend::{db_init, get_config, get_now_iso, read_csv, Accounts, Config}; use sqlx::{Pool, Sqlite}; @@ -14,11 +13,12 @@ async fn main() -> tide::Result<()> { } async fn update(config: &Config) -> tide::Result<()> { - dotenv().ok(); + let db = db_init(config).await.unwrap(); - let mut users_tmp = HashSet::new(); // default user to ensure group is never empty - users_tmp.insert(String::from("compsoc")); + 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::>() { @@ -32,22 +32,18 @@ async fn update(config: &Config) -> tide::Result<()> { } if let Ok(x) = env::var("USERS_ADMIN") { - let users = x.split(',').collect::>(); - - update_group(config, "skynet-admins", &users, true).await?; // admins automatically get added as users - for user in 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") { - let users = x.split(',').collect::>(); - - update_group(config, "skynet-committee", &users, true).await?; // committee automatically get added as users - for user in users { + for user in x.split(',').collect::>() { + committee_tmp.insert(user.to_string()); users_tmp.insert(user.to_string()); } } @@ -59,10 +55,15 @@ async fn update(config: &Config) -> tide::Result<()> { } } - // easier to work with Strings above but easier to work with &str below - let users: Vec<&str> = users_tmp.iter().map(|s| &**s).collect(); + 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?; Ok(()) } @@ -71,7 +72,7 @@ fn uid_to_dn(uid: &str) -> String { format!("uid={},ou=users,dc=skynet,dc=ie", uid) } -async fn update_group(config: &Config, group: &str, users: &[&str], replace: bool) -> tide::Result<()> { +async fn update_group(config: &Config, group: &str, users: &Vec, replace: bool) -> tide::Result<()> { if users.is_empty() { return Ok(()); } @@ -178,3 +179,56 @@ async fn account_id_get_uid(db: &Pool, id: &str) -> Option { Err(_) => None, } } + +struct AccountsSecure { + 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, + } +} + +async fn get_secure_sub(db: &Pool, group: &HashSet, cache: &mut HashSet) -> 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()); + } + } + + tmp +} +async fn is_secure(db: &Pool, user: &str) -> bool { + 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, + } +} From 61ccaf0bdb70448b4972339053a761cdc8c4b8d7 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sat, 5 Aug 2023 17:36:48 +0100 Subject: [PATCH 6/6] feat: returns the user details on successful account modification Closes #8 --- README.md | 14 ++++++++++ src/methods/account_update.rs | 50 ++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4392de7..365ea29 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,20 @@ Fields: } ``` +Success: +Each value is either a string or ``null``. +```json +{ + "result": "success", + "success": { + "cn": "Firstname Surname", + "mail": "Email address", + "skDiscord": null, + "sshPublicKey": "ssh key" + } +} +``` + Changing ``userPassword`` requires the existing password in teh apssword field and the new one in teh value field. ### POST /ldap/new diff --git a/src/methods/account_update.rs b/src/methods/account_update.rs index 9dd0f27..4cbbea6 100644 --- a/src/methods/account_update.rs +++ b/src/methods/account_update.rs @@ -1,8 +1,8 @@ use crate::State; use ldap3::{exop::PasswordModify, LdapConn, Mod, Scope, SearchEntry}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use tide::{ - prelude::{json, Deserialize}, + prelude::{json, Deserialize, Serialize}, Request, }; @@ -14,6 +14,16 @@ pub struct LdapUpdate { 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, +} + /// Handles updating a single field with the users own password pub async fn post_update_ldap(mut req: Request) -> tide::Result { let LdapUpdate { @@ -80,7 +90,41 @@ pub async fn post_update_ldap(mut req: Request) -> tide::Result { ldap.extended(tmp)?.success()?; }; + let result = get_result(&mut ldap, &dn); + ldap.unbind()?; - Ok(json!({"result": "success"}).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, + }; + + 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 +} + +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 }