From bc467368630e66e8b630b4e0440086916141dc87 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 20:46:44 +0100 Subject: [PATCH 01/18] fix: add an index for expirey --- src/lib.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2bd7bea..dd2fa11 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,9 +57,8 @@ pub async fn db_init(config: &Config) -> Result, Error> { .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_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?; // this is for active use sqlx::query( From a851a81decdd9ca06894def5f9b14ea3cd383c4c Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 21:14:36 +0100 Subject: [PATCH 02/18] feat: started teh password reset option --- src/lib.rs | 34 ++++- src/methods/mod.rs | 1 + src/methods/password_reset.rs | 227 ++++++++++++++++++++++++++++++++++ 3 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 src/methods/password_reset.rs diff --git a/src/lib.rs b/src/lib.rs index dd2fa11..1c2f710 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,14 @@ pub struct AccountsNew { pub id_student: String, } -#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +#[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)] +pub struct AccountsReset { + pub user: String, + pub auth_code: String, + pub date_expiry: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)] pub struct Accounts { pub user: String, pub uid: i64, @@ -57,8 +64,29 @@ pub async fn db_init(config: &Config) -> Result, Error> { .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_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?; // this is for active use sqlx::query( diff --git a/src/methods/mod.rs b/src/methods/mod.rs index f4ba1d1..f223eda 100644 --- a/src/methods/mod.rs +++ b/src/methods/mod.rs @@ -1,2 +1,3 @@ pub mod account_new; pub mod account_update; +pub mod password_reset; diff --git a/src/methods/password_reset.rs b/src/methods/password_reset.rs new file mode 100644 index 0000000..64589a2 --- /dev/null +++ b/src/methods/password_reset.rs @@ -0,0 +1,227 @@ +use crate::{get_now_iso, random_string, Accounts, AccountsReset, Config, State}; +use chrono::{Duration, SecondsFormat, Utc}; +use lettre::{ + 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, +}; + +#[derive(Debug, Deserialize)] +pub struct PassReset { + user: Option, + email: Option, +} + +/// Handles password resets +/// All responses are success, never want to leak info +pub async fn post_password_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": "success"}).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, &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") { + 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()) +} + +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 +} + +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 +} + +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 +} + +fn send_mail(config: &Config, record: &Accounts, auth: &str) -> Result { + let recipient = &record.user; + let mail = &record.mail; + let url_base = "https://sso.skynet.ie"; + let link_new = format!("{url_base}/reset_pw?auth={auth}"); + let discord = "https://discord.gg/mkuKJkCuyM"; + 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) } + } + 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 {recipient} + + Here is your password reset link: + {link_new} + + If did not request this please ignore. + + UL Computer Society + 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"); + + 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(); + + // 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); + + 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 +} From 712cd50ff24c9ee0edf2ccde4a97fa02a81aa0b8 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 22:56:02 +0100 Subject: [PATCH 03/18] feat: handle the receiving of the token --- src/bin/update_groups.rs | 6 +-- src/lib.rs | 4 ++ src/main.rs | 8 +++- src/methods/password_reset.rs | 77 ++++++++++++++++++++++++++++++++++- 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/src/bin/update_groups.rs b/src/bin/update_groups.rs index e1c4e9d..6d6761c 100644 --- a/src/bin/update_groups.rs +++ b/src/bin/update_groups.rs @@ -1,6 +1,6 @@ use dotenvy::dotenv; use ldap3::{LdapConn, Mod}; -use skynet_ldap_backend::{db_init, get_config, get_now_iso, read_csv, Accounts, Config}; +use skynet_ldap_backend::{db_init, get_config, get_now_iso, read_csv, uid_to_dn, Accounts, Config}; use sqlx::{Pool, Sqlite}; use std::{collections::HashSet, env, error::Error}; @@ -46,10 +46,6 @@ async fn update_users(config: &Config) -> tide::Result<()> { Ok(()) } -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(); diff --git a/src/lib.rs b/src/lib.rs index 1c2f710..5994ae2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -310,3 +310,7 @@ pub fn read_csv(config: &Config) -> Result, Box String { thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect() } + +pub fn uid_to_dn(uid: &str) -> String { + format!("uid={},ou=users,dc=skynet,dc=ie", uid) +} diff --git a/src/main.rs b/src/main.rs index 9cd988f..617f356 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,10 @@ use skynet_ldap_backend::{ db_init, get_config, - methods::{account_new::post_new_account, account_update::post_update_ldap}, + methods::{ + account_new::post_new_account, + account_update::post_update_ldap, + password_reset::{post_password_auth, post_password_reset}, + }, State, }; @@ -22,6 +26,8 @@ async fn main() -> tide::Result<()> { app.at("/ldap/update").post(post_update_ldap); app.at("/ldap/new").post(post_new_account); + app.at("/ldap/reset").post(post_password_reset); + app.at("/ldap/reset/auth").post(post_password_auth); app.listen(host_port).await?; Ok(()) diff --git a/src/methods/password_reset.rs b/src/methods/password_reset.rs index 64589a2..b41ea03 100644 --- a/src/methods/password_reset.rs +++ b/src/methods/password_reset.rs @@ -1,5 +1,6 @@ -use crate::{get_now_iso, random_string, Accounts, AccountsReset, Config, State}; +use crate::{get_now_iso, random_string, uid_to_dn, Accounts, AccountsReset, Config, State}; use chrono::{Duration, SecondsFormat, Utc}; +use ldap3::{exop::PasswordModify, LdapConn}; use lettre::{ message::{header, MultiPart, SinglePart}, transport::smtp::{authentication::Credentials, response::Response, Error}, @@ -70,6 +71,40 @@ pub async fn post_password_reset(mut req: Request) -> tide::Result { Ok(json!({"result": "success"}).into()) } +#[derive(Debug, Deserialize)] +pub struct PassResetAuth { + auth: String, + pass: String, +} + +pub async fn post_password_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": "success"}).into()); + } + + // check if auth exists + let details = match db_get_user_reset_auth(db, &auth).await { + None => { + return Ok(json!({"result": "success"}).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()) +} + async fn db_get_user(pool: &Pool, user_in: &Option, mail_in: &Option) -> Option { let user = match user_in { None => "", @@ -133,6 +168,46 @@ async fn db_get_user_reset(pool: &Pool, user: &str) -> Option, 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()); + } + } + + 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; From e46ae563e0c57ce9984e80b98bcb8669d48725d3 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 22:58:52 +0100 Subject: [PATCH 04/18] fmt: smol cleanup Related to #4 --- src/main.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 617f356..07298aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,3 @@ async fn main() -> tide::Result<()> { app.listen(host_port).await?; Ok(()) } - -/* -Password reset via email -*/ From bfe96f0f2c70f9946a9f41e9a229d35698dd2b07 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 23:14:13 +0100 Subject: [PATCH 05/18] test: bump for pipeline --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 6eaea8c..605df1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,3 +41,4 @@ csv = "1.2" # for email lettre = "0.10.4" maud = "0.25.0" + From 04853f439bfc6e7e1811a4845ad9cb0249cebe69 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 23:22:40 +0100 Subject: [PATCH 06/18] doc: added documentation for the path --- README.md | 43 +++++++++++++++++++++++++++++++++++ src/methods/password_reset.rs | 4 ++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4392de7..b1e8da4 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,49 @@ Invalid Auth: Generic responses which is used unless otherwise specified above. +### POST /ldap/reset + +```json +{ + "user" : "[OPTIONAL] username looking for reset", + "email" : "[OPTIONAL] email looking for reset" +} +``` + +All responses: +```json +{"result": "success"} +``` + +### POST /ldap/reset/auth + +```json +{ + "auth" : "Auth key from teh email", + "pass" : "Password the user chooses" +} +``` + +Early Errors: +```json +{"result": "error"} +``` + +LDAP error: +```json +{"result": "error", "error": "ldap error"} +``` + +Success: +```json +{"result": "success", "success": "Password set"} +``` + +## Responses + +Generic responses which is used unless otherwise specified above. + + ### Success: HTTP 200 ```json { diff --git a/src/methods/password_reset.rs b/src/methods/password_reset.rs index b41ea03..6d3dd58 100644 --- a/src/methods/password_reset.rs +++ b/src/methods/password_reset.rs @@ -87,13 +87,13 @@ pub async fn post_password_auth(mut req: Request) -> tide::Result { let db = &req.state().db; if db_pending_clear_expired(db).await.is_err() { - return Ok(json!({"result": "success"}).into()); + 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": "success"}).into()); + return Ok(json!({"result": "error"}).into()); } Some(x) => x, }; From 0605bf5e4da9956c430a567732c3b15ab0050fce Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sat, 5 Aug 2023 17:44:54 +0100 Subject: [PATCH 07/18] fix: use the more broad term recovery --- src/methods/password_reset.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/methods/password_reset.rs b/src/methods/password_reset.rs index 6d3dd58..60da73a 100644 --- a/src/methods/password_reset.rs +++ b/src/methods/password_reset.rs @@ -212,7 +212,7 @@ fn send_mail(config: &Config, record: &Accounts, auth: &str) -> Result", &config.mail_user); From 2bd68afe28685786080fcf0b08b528e57a7bd67c Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 20:46:44 +0100 Subject: [PATCH 08/18] fix: add an index for expirey --- src/lib.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 92d30b2..05a11b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,9 +80,8 @@ pub async fn db_init(config: &Config) -> Result, Error> { .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_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?; // this is for active use sqlx::query( From 3a5b96c4d909cc2647f30ebcfb39a2dcc0e0c44b Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 21:14:36 +0100 Subject: [PATCH 09/18] feat: started teh password reset option --- src/lib.rs | 34 ++++- src/methods/mod.rs | 1 + src/methods/password_reset.rs | 227 ++++++++++++++++++++++++++++++++++ 3 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 src/methods/password_reset.rs diff --git a/src/lib.rs b/src/lib.rs index 05a11b0..1a15fc7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,14 @@ pub struct AccountsNew { pub id_student: String, } -#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +#[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)] +pub struct AccountsReset { + pub user: String, + pub auth_code: String, + pub date_expiry: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)] pub struct Accounts { pub user: String, pub uid: i64, @@ -80,8 +87,29 @@ pub async fn db_init(config: &Config) -> Result, Error> { .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_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?; // this is for active use sqlx::query( diff --git a/src/methods/mod.rs b/src/methods/mod.rs index f4ba1d1..f223eda 100644 --- a/src/methods/mod.rs +++ b/src/methods/mod.rs @@ -1,2 +1,3 @@ pub mod account_new; pub mod account_update; +pub mod password_reset; diff --git a/src/methods/password_reset.rs b/src/methods/password_reset.rs new file mode 100644 index 0000000..64589a2 --- /dev/null +++ b/src/methods/password_reset.rs @@ -0,0 +1,227 @@ +use crate::{get_now_iso, random_string, Accounts, AccountsReset, Config, State}; +use chrono::{Duration, SecondsFormat, Utc}; +use lettre::{ + 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, +}; + +#[derive(Debug, Deserialize)] +pub struct PassReset { + user: Option, + email: Option, +} + +/// Handles password resets +/// All responses are success, never want to leak info +pub async fn post_password_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": "success"}).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, &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") { + 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()) +} + +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 +} + +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 +} + +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 +} + +fn send_mail(config: &Config, record: &Accounts, auth: &str) -> Result { + let recipient = &record.user; + let mail = &record.mail; + let url_base = "https://sso.skynet.ie"; + let link_new = format!("{url_base}/reset_pw?auth={auth}"); + let discord = "https://discord.gg/mkuKJkCuyM"; + 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) } + } + 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 {recipient} + + Here is your password reset link: + {link_new} + + If did not request this please ignore. + + UL Computer Society + 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"); + + 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(); + + // 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); + + 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 +} From 4bdfa09ee313bbf4b757fce2f0236c84e712a1fb Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 22:56:02 +0100 Subject: [PATCH 10/18] feat: handle the receiving of the token --- src/lib.rs | 4 ++ src/main.rs | 3 ++ src/methods/password_reset.rs | 77 ++++++++++++++++++++++++++++++++++- 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 1a15fc7..84e8464 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -239,3 +239,7 @@ pub async fn get_wolves(db: &Pool) -> Vec { .await .unwrap_or(vec![]) } + +pub fn uid_to_dn(uid: &str) -> String { + format!("uid={},ou=users,dc=skynet,dc=ie", uid) +} diff --git a/src/main.rs b/src/main.rs index 1634a36..11427be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use skynet_ldap_backend::{ methods::{ account_new::post::{account, email}, account_update::post_update_ldap, + password_reset::{post_password_auth, post_password_reset}, }, State, }; @@ -26,6 +27,8 @@ async fn main() -> tide::Result<()> { app.at("/ldap/update").post(post_update_ldap); app.at("/ldap/new/email").post(email::submit); app.at("/ldap/new/account").post(account::submit); + app.at("/ldap/reset").post(post_password_reset); + app.at("/ldap/reset/auth").post(post_password_auth); app.listen(host_port).await?; Ok(()) diff --git a/src/methods/password_reset.rs b/src/methods/password_reset.rs index 64589a2..b41ea03 100644 --- a/src/methods/password_reset.rs +++ b/src/methods/password_reset.rs @@ -1,5 +1,6 @@ -use crate::{get_now_iso, random_string, Accounts, AccountsReset, Config, State}; +use crate::{get_now_iso, random_string, uid_to_dn, Accounts, AccountsReset, Config, State}; use chrono::{Duration, SecondsFormat, Utc}; +use ldap3::{exop::PasswordModify, LdapConn}; use lettre::{ message::{header, MultiPart, SinglePart}, transport::smtp::{authentication::Credentials, response::Response, Error}, @@ -70,6 +71,40 @@ pub async fn post_password_reset(mut req: Request) -> tide::Result { Ok(json!({"result": "success"}).into()) } +#[derive(Debug, Deserialize)] +pub struct PassResetAuth { + auth: String, + pass: String, +} + +pub async fn post_password_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": "success"}).into()); + } + + // check if auth exists + let details = match db_get_user_reset_auth(db, &auth).await { + None => { + return Ok(json!({"result": "success"}).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()) +} + async fn db_get_user(pool: &Pool, user_in: &Option, mail_in: &Option) -> Option { let user = match user_in { None => "", @@ -133,6 +168,46 @@ async fn db_get_user_reset(pool: &Pool, user: &str) -> Option, 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()); + } + } + + 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; From d0ddc79c4c037bf85980d183b3fc63f40f18cc20 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 22:58:52 +0100 Subject: [PATCH 11/18] fmt: smol cleanup Related to #4 --- src/main.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 11427be..1a5ab56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,7 +33,3 @@ async fn main() -> tide::Result<()> { app.listen(host_port).await?; Ok(()) } - -/* -Password reset via email -*/ From ca2f23915a3ff6129c33e23425593f0afaa669c6 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 23:14:13 +0100 Subject: [PATCH 12/18] test: bump for pipeline --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index e934ae3..bc1bf5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,3 +40,4 @@ csv = "1.2" # for email lettre = "0.10.4" maud = "0.25.0" + From d10d40522e48b4cf301c541b3aeae992699fc8f6 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 30 Jul 2023 23:22:40 +0100 Subject: [PATCH 13/18] doc: added documentation for the path --- README.md | 43 +++++++++++++++++++++++++++++++++++ src/methods/password_reset.rs | 4 ++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fff41e1..215dce5 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,49 @@ Invalid Auth: Generic responses which is used unless otherwise specified above. +### POST /ldap/reset + +```json +{ + "user" : "[OPTIONAL] username looking for reset", + "email" : "[OPTIONAL] email looking for reset" +} +``` + +All responses: +```json +{"result": "success"} +``` + +### POST /ldap/reset/auth + +```json +{ + "auth" : "Auth key from teh email", + "pass" : "Password the user chooses" +} +``` + +Early Errors: +```json +{"result": "error"} +``` + +LDAP error: +```json +{"result": "error", "error": "ldap error"} +``` + +Success: +```json +{"result": "success", "success": "Password set"} +``` + +## Responses + +Generic responses which is used unless otherwise specified above. + + ### Success: HTTP 200 ```json { diff --git a/src/methods/password_reset.rs b/src/methods/password_reset.rs index b41ea03..6d3dd58 100644 --- a/src/methods/password_reset.rs +++ b/src/methods/password_reset.rs @@ -87,13 +87,13 @@ pub async fn post_password_auth(mut req: Request) -> tide::Result { let db = &req.state().db; if db_pending_clear_expired(db).await.is_err() { - return Ok(json!({"result": "success"}).into()); + 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": "success"}).into()); + return Ok(json!({"result": "error"}).into()); } Some(x) => x, }; From bfa88b72b782e28f7f9a37f905d388f72211ba96 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sat, 5 Aug 2023 17:44:54 +0100 Subject: [PATCH 14/18] fix: use the more broad term recovery --- src/methods/password_reset.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/methods/password_reset.rs b/src/methods/password_reset.rs index 6d3dd58..60da73a 100644 --- a/src/methods/password_reset.rs +++ b/src/methods/password_reset.rs @@ -212,7 +212,7 @@ fn send_mail(config: &Config, record: &Accounts, auth: &str) -> Result", &config.mail_user); From 3165f67e4c4132672451a1e5c17e186f6b37c67e Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 6 Aug 2023 14:39:45 +0100 Subject: [PATCH 15/18] fix: better layout using modules --- src/main.rs | 14 +- src/methods/account_recover.rs | 306 +++++++++++++++++++++++++++++++++ src/methods/mod.rs | 2 +- src/methods/password_reset.rs | 302 -------------------------------- 4 files changed, 312 insertions(+), 312 deletions(-) create mode 100644 src/methods/account_recover.rs delete mode 100644 src/methods/password_reset.rs diff --git a/src/main.rs b/src/main.rs index 1a5ab56..c93a7e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,6 @@ use skynet_ldap_backend::{ db_init, get_config, - methods::{ - account_new::post::{account, email}, - account_update::post_update_ldap, - password_reset::{post_password_auth, post_password_reset}, - }, + methods::{account_new, account_recover, account_update::post_update_ldap}, State, }; @@ -25,10 +21,10 @@ async fn main() -> tide::Result<()> { let mut app = tide::with_state(state); app.at("/ldap/update").post(post_update_ldap); - app.at("/ldap/new/email").post(email::submit); - app.at("/ldap/new/account").post(account::submit); - app.at("/ldap/reset").post(post_password_reset); - app.at("/ldap/reset/auth").post(post_password_auth); + app.at("/ldap/new/email").post(account_new::post::email::submit); + app.at("/ldap/new/account").post(account_new::post::account::submit); + app.at("/ldap/recover/password").post(account_recover::password::reset); + app.at("/ldap/recover/password/auth").post(account_recover::password::auth); app.listen(host_port).await?; Ok(()) diff --git a/src/methods/account_recover.rs b/src/methods/account_recover.rs new file mode 100644 index 0000000..22f3558 --- /dev/null +++ b/src/methods/account_recover.rs @@ -0,0 +1,306 @@ +use crate::{get_now_iso, random_string, uid_to_dn, Accounts, AccountsReset, Config, State}; +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, +}; +use maud::html; +use sqlx::{Pool, Sqlite}; +use tide::{ + prelude::{json, Deserialize}, + Request, +}; + +pub mod password { + use super::*; + + #[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": "success"}).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, &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") { + 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()) + } + + #[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()) + } + + 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 + } + + 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 + } + + 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 + } + + 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()); + } + } + + 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://sso.skynet.ie"; + let link_new = format!("{url_base}/recovery_pass?auth={auth}"); + let discord = "https://discord.gg/mkuKJkCuyM"; + 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) } + } + 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 {recipient} + + Here is your password reset link: + {link_new} + + If did not request this please ignore. + + UL Computer Society + 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"); + + 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(); + + // 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); + + 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 + } +} diff --git a/src/methods/mod.rs b/src/methods/mod.rs index f223eda..8273b7b 100644 --- a/src/methods/mod.rs +++ b/src/methods/mod.rs @@ -1,3 +1,3 @@ pub mod account_new; +pub mod account_recover; pub mod account_update; -pub mod password_reset; diff --git a/src/methods/password_reset.rs b/src/methods/password_reset.rs deleted file mode 100644 index 60da73a..0000000 --- a/src/methods/password_reset.rs +++ /dev/null @@ -1,302 +0,0 @@ -use crate::{get_now_iso, random_string, uid_to_dn, Accounts, AccountsReset, Config, State}; -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, -}; -use maud::html; -use sqlx::{Pool, Sqlite}; -use tide::{ - prelude::{json, Deserialize}, - Request, -}; - -#[derive(Debug, Deserialize)] -pub struct PassReset { - user: Option, - email: Option, -} - -/// Handles password resets -/// All responses are success, never want to leak info -pub async fn post_password_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": "success"}).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, &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") { - 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()) -} - -#[derive(Debug, Deserialize)] -pub struct PassResetAuth { - auth: String, - pass: String, -} - -pub async fn post_password_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()) -} - -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 -} - -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 -} - -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 -} - -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()); - } - } - - 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://sso.skynet.ie"; - let link_new = format!("{url_base}/recovery_pass?auth={auth}"); - let discord = "https://discord.gg/mkuKJkCuyM"; - 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) } - } - 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 {recipient} - - Here is your password reset link: - {link_new} - - If did not request this please ignore. - - UL Computer Society - 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"); - - 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(); - - // 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); - - 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 -} From 9d28d89eee5414333a636f9356d1c0ce3a6c6f29 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 6 Aug 2023 14:42:09 +0100 Subject: [PATCH 16/18] fix: better strucurter in other modules --- src/main.rs | 8 +- src/methods/account_new.rs | 662 +++++++++++++++++----------------- src/methods/account_update.rs | 2 +- 3 files changed, 334 insertions(+), 338 deletions(-) diff --git a/src/main.rs b/src/main.rs index c93a7e6..30b436c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use skynet_ldap_backend::{ db_init, get_config, - methods::{account_new, account_recover, account_update::post_update_ldap}, + methods::{account_new, account_recover, account_update}, State, }; @@ -20,9 +20,9 @@ async fn main() -> tide::Result<()> { let mut app = tide::with_state(state); - app.at("/ldap/update").post(post_update_ldap); - app.at("/ldap/new/email").post(account_new::post::email::submit); - app.at("/ldap/new/account").post(account_new::post::account::submit); + app.at("/ldap/update").post(account_update::submit); + app.at("/ldap/new/email").post(account_new::email::submit); + app.at("/ldap/new/account").post(account_new::account::submit); app.at("/ldap/recover/password").post(account_recover::password::reset); app.at("/ldap/recover/password/auth").post(account_recover::password::auth); diff --git a/src/methods/account_new.rs b/src/methods/account_new.rs index 0026076..3948aab 100644 --- a/src/methods/account_new.rs +++ b/src/methods/account_new.rs @@ -13,161 +13,158 @@ use tide::{ Request, }; -pub mod post { +pub mod email { use super::*; - pub mod email { - 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; - } - - // 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); - - 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); - } - } + for record in get_wolves_mail(db, &email).await { + // skynet emails not permitted + if record.email.trim().ends_with("@skynet.ie") { + continue; } - Ok(json!({"result": "success"}).into()) + // 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); + + 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); + } + } } - async fn get_wolves_mail(db: &Pool, mail: &str) -> Vec { - sqlx::query_as::<_, AccountWolves>( - r#" + Ok(json!({"result": "success"}).into()) + } + + 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://sso.skynet.ie"; - let link_new = format!("{url_base}/register?auth={auth}"); - let link_mod = format!("{url_base}/modify"); - let discord = "https://discord.gg/mkuKJkCuyM"; - 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://sso.skynet.ie"; + let link_new = format!("{url_base}/register?auth={auth}"); + let link_mod = format!("{url_base}/modify"); + let discord = "https://discord.gg/mkuKJkCuyM"; + 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. @@ -189,270 +186,269 @@ pub mod post { 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::*; + + #[derive(Debug, Deserialize)] + struct LdapNewUser { + auth: String, + user: String, + pass: String, } - pub mod account { - use super::*; + /// 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?; - #[derive(Debug, Deserialize)] - struct LdapNewUser { - auth: String, - user: String, - pass: String, + 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()); - } - - // 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()); - } + 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")); } - 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 _")); } - - 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()); - } + ) + .bind(auth) + .fetch_all(pool) + .await + { + if !res.is_empty() { + return Some(res[0].to_owned()); } - - None } - 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), + }; - ldap.unbind()?; + ldap.extended(tmp).unwrap(); - Ok(()) - } + ldap.unbind()?; - fn get_sk_created() -> String { - use chrono::Utc; - let now = Utc::now(); + Ok(()) + } - format!("{}", now.format("%Y%m%d%H%M%SZ")) - } + fn get_sk_created() -> String { + use chrono::Utc; + let now = Utc::now(); - async fn get_max_uid_number(db: &Pool) -> i64 { - if let Ok(results) = sqlx::query_as::<_, Accounts>( - r#" + 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; - } + ) + .fetch_all(db) + .await + { + if !results.is_empty() { + return results[0].uid + 1; } - - 9999 } - 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_update.rs b/src/methods/account_update.rs index 4cbbea6..7d0a90b 100644 --- a/src/methods/account_update.rs +++ b/src/methods/account_update.rs @@ -25,7 +25,7 @@ pub struct ModifyResult { } /// Handles updating a single field with the users own password -pub async fn post_update_ldap(mut req: Request) -> tide::Result { +pub async fn submit(mut req: Request) -> tide::Result { let LdapUpdate { user, pass, From b48a8fc711b06bf3a0d876d145291feb73bfbbcf Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 6 Aug 2023 14:43:49 +0100 Subject: [PATCH 17/18] doc: update documentation and comments --- README.md | 4 ++-- src/main.rs | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 215dce5..2be985f 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Invalid Auth: Generic responses which is used unless otherwise specified above. -### POST /ldap/reset +### POST /ldap/recover/password ```json { @@ -95,7 +95,7 @@ All responses: {"result": "success"} ``` -### POST /ldap/reset/auth +### POST /ldap/recover/password/auth ```json { diff --git a/src/main.rs b/src/main.rs index 30b436c..1f7c3ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,9 +20,14 @@ async fn main() -> tide::Result<()> { let mut app = tide::with_state(state); + // 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 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); From 0b385cafab32bb1adc32abbc029eabab065edb3d Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Sun, 6 Aug 2023 18:11:07 +0100 Subject: [PATCH 18/18] fix: fmt and clippy --- src/lib.rs | 24 +++++++++++++++--------- src/main.rs | 4 ++-- src/methods/account_new.rs | 10 +++++----- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e407749..03e5a51 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,8 +88,12 @@ pub async fn db_init(config: &Config) -> Result, Error> { .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_reset ( @@ -101,8 +105,12 @@ pub async fn db_init(config: &Config) -> Result, Error> { .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( @@ -121,7 +129,9 @@ pub async fn db_init(config: &Config) -> Result, Error> { 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_student_id ON accounts (student_id)") + .execute(&pool) + .await?; Ok(pool) } @@ -273,7 +283,3 @@ pub async fn update_group(config: &Config, group: &str, users: &Vec, rep Ok(()) } - -pub fn uid_to_dn(uid: &str) -> String { - format!("uid={},ou=users,dc=skynet,dc=ie", uid) -} diff --git a/src/main.rs b/src/main.rs index 1f7c3ff..e3992ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,11 +22,11 @@ async fn main() -> tide::Result<()> { // 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 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); diff --git a/src/methods/account_new.rs b/src/methods/account_new.rs index ebbdd63..4e40b46 100644 --- a/src/methods/account_new.rs +++ b/src/methods/account_new.rs @@ -409,12 +409,12 @@ pub mod account { ldap.extended(tmp).unwrap(); - // user is already verified by being an active member on wolves - if let Err(e) = update_group(config, "skynet-users", &vec![username.to_string()], true).await { - println!("Couldnt add {} to skynet-users: {:?}", username, e) - } + // user is already verified by being an active member on wolves + if let Err(e) = update_group(config, "skynet-users", &vec![username.to_string()], true).await { + println!("Couldnt add {} to skynet-users: {:?}", username, e) + } - ldap.unbind()?; + ldap.unbind()?; Ok(()) }