finga
7b0e4b4a31
On the `reset` page an email address can be submitted. If an account associated with the submitted email address an email is sent containing an URL. This URL can be used to set a new password. - Add GPLv3 for licensing - Add dependencies - `rocket_contrib` to be able to use handlebar templates - `anyhow` to handle errors - `log` for logging - `ldap3` to communicate with a LDAP server - `lettre` and `lettre_email` to handle the generation of emails and to send them - `rand` to generate random keys - Add `README.org` which is also used to generate `README.md` - Add configuration parameters - domain - LDAP - server - base - filter - bind - password - Change default development address to 0.0.0.0 - Add structs to handle data - Add functions to handle password reset actions - `reset_prepare()` to generate a new key, send it to the requestor and keep it in the memory - `set_password()` to check for the key and set the password - Add routes - Add tests - Add templates - `reset.html.hbs` to submit an email address - `reset_key.html.hbs` to set the new password
323 lines
9.3 KiB
Rust
323 lines
9.3 KiB
Rust
#![feature(proc_macro_hygiene, decl_macro)]
|
|
|
|
#[macro_use]
|
|
extern crate rocket;
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use anyhow::{anyhow, Result};
|
|
use ldap3::{
|
|
controls::{MakeCritical, RelaxRules},
|
|
LdapConn, Mod, Scope, SearchEntry,
|
|
};
|
|
use lettre::{SmtpClient, Transport};
|
|
use lettre_email::EmailBuilder;
|
|
use log::{debug, error, info};
|
|
use rand::{self, Rng};
|
|
use rocket::{
|
|
fairing::AdHoc,
|
|
request::{FlashMessage, Form},
|
|
response::{Flash, Redirect},
|
|
State,
|
|
};
|
|
use rocket_contrib::templates::Template;
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
struct LdapConfig {
|
|
server: String,
|
|
base: String,
|
|
filter: String,
|
|
bind: String,
|
|
password: String,
|
|
}
|
|
|
|
struct Ldap0rConfig {
|
|
domain: String,
|
|
ldap: LdapConfig,
|
|
}
|
|
|
|
const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
|
|
struct Keys {
|
|
keys: Arc<Mutex<HashMap<String, String>>>,
|
|
}
|
|
|
|
fn reset_prepare(config: &Ldap0rConfig, keys: &Keys, email_address: &str) -> Result<()> {
|
|
// ldap lookup
|
|
let mut ldap = LdapConn::new(&config.ldap.server)?;
|
|
let result = ldap.search(
|
|
&config.ldap.base,
|
|
Scope::Subtree,
|
|
&format!("(&{}(mail={}))", config.ldap.filter, email_address),
|
|
vec![""],
|
|
)?;
|
|
ldap.unbind()?;
|
|
let (rs, _res) = result.success()?;
|
|
|
|
if rs.len() == 1 {
|
|
// generate key
|
|
let mut key = String::with_capacity(64);
|
|
let mut rng = rand::thread_rng();
|
|
for _ in 0..64 {
|
|
key.push(BASE62[rng.gen::<usize>() % 62] as char);
|
|
}
|
|
|
|
// store key with id
|
|
let keys = Arc::clone(&keys.keys);
|
|
if let Ok(mut keys) = keys.lock() {
|
|
keys.insert(key.to_string(), email_address.to_string());
|
|
debug!("Generated new key for '{}'", email_address);
|
|
|
|
// generate email
|
|
let email = EmailBuilder::new()
|
|
.to(email_address)
|
|
.from("ldap0r@example.com")
|
|
.subject("LDAP password reset")
|
|
.text(format!(
|
|
"Use following url to set a new password: {}/reset/{}",
|
|
config.domain, key
|
|
))
|
|
.build()?;
|
|
|
|
// send email
|
|
let mut mailer = SmtpClient::new_unencrypted_localhost()?.transport();
|
|
let result = mailer.send(email.into());
|
|
|
|
if result.is_ok() {
|
|
info!("Password reset email was sent to '{}'", email_address);
|
|
} else {
|
|
error!(
|
|
"Sending password reset email with reset URL to '{}' failed",
|
|
email_address
|
|
);
|
|
}
|
|
} else {
|
|
error!("Could not aquire lock for keys");
|
|
};
|
|
} else {
|
|
error!("Invalid password reset request for '{}'", email_address);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn set_password(
|
|
config: &Ldap0rConfig,
|
|
keys: &Keys,
|
|
key: &str,
|
|
passwords: &PasswordsForm,
|
|
) -> Result<Flash<Redirect>> {
|
|
if passwords.password == passwords.password_control {
|
|
if passwords.password.len() >= 8 {
|
|
// key lookup
|
|
let keys = Arc::clone(&keys.keys);
|
|
if let Ok(mut keys) = keys.lock() {
|
|
let email = keys
|
|
.get(key)
|
|
.ok_or_else(|| anyhow!("Could not extract email"))?.to_string();
|
|
|
|
// ldap lookup
|
|
let mut ldap = LdapConn::new(&config.ldap.server)?;
|
|
let result = ldap.search(
|
|
&config.ldap.base,
|
|
Scope::Subtree,
|
|
&format!("(&{}(mail={}))", &config.ldap.filter, &email),
|
|
vec!["cn"],
|
|
)?;
|
|
let (mut rs, _res) = result.success()?;
|
|
|
|
// ldap set new password
|
|
let user = SearchEntry::construct(
|
|
rs.pop()
|
|
.ok_or_else(|| anyhow!("Could extract not receive LDAP result"))?,
|
|
)
|
|
.attrs
|
|
.get("cn")
|
|
.ok_or_else(|| anyhow!("Could not extract 'cn' from LDAP entry"))?[0]
|
|
.to_string();
|
|
let mut password = HashSet::new();
|
|
password.insert(passwords.password.as_str());
|
|
ldap.simple_bind(&config.ldap.bind, &config.ldap.password)?
|
|
.success()?;
|
|
ldap.with_controls(RelaxRules.critical())
|
|
.modify(
|
|
&format!("cn={},{}", &user, &config.ldap.base),
|
|
vec![Mod::Replace("userPassword", password)],
|
|
)?
|
|
.success()?;
|
|
|
|
ldap.unbind()?;
|
|
keys.remove(key);
|
|
|
|
info!(
|
|
"New password set for user '{}' with email address '{}'",
|
|
&user, &email
|
|
);
|
|
return Ok(Flash::success(
|
|
Redirect::to(uri!(reset)),
|
|
"New password was saved",
|
|
));
|
|
} else {
|
|
error!("Could not aquire lock for keys");
|
|
};
|
|
} else {
|
|
return Ok(Flash::error(
|
|
Redirect::to(uri!(reset_key: key)),
|
|
"Password length has to be at least 8",
|
|
));
|
|
}
|
|
} else {
|
|
return Ok(Flash::error(
|
|
Redirect::to(uri!(reset_key: key)),
|
|
"Password does not match the password verification field",
|
|
));
|
|
}
|
|
|
|
Ok(Flash::error(
|
|
Redirect::to(uri!(reset_key: key)),
|
|
"Setting new password failed",
|
|
))
|
|
}
|
|
|
|
#[derive(Debug, FromForm)]
|
|
struct EmailForm {
|
|
email: String,
|
|
}
|
|
|
|
#[derive(Debug, FromForm)]
|
|
struct PasswordsForm {
|
|
password: String,
|
|
password_control: String,
|
|
}
|
|
|
|
#[get("/")]
|
|
fn index() -> Redirect {
|
|
Redirect::to(uri!(reset))
|
|
}
|
|
|
|
#[get("/reset")]
|
|
fn reset(flash: Option<FlashMessage>) -> Template {
|
|
let mut context = HashMap::new();
|
|
|
|
if let Some(ref msg) = flash {
|
|
context.insert("flash", msg.msg());
|
|
if msg.name() == "error" {
|
|
context.insert("flash_type", "Error");
|
|
}
|
|
}
|
|
|
|
Template::render("reset", &context)
|
|
}
|
|
|
|
#[post("/reset", data = "<email>")]
|
|
fn reset_email(
|
|
config: State<Ldap0rConfig>,
|
|
keys: State<Keys>,
|
|
email: Form<EmailForm>,
|
|
) -> Flash<Redirect> {
|
|
if let Err(e) = reset_prepare(&config, &keys, &email.email) {
|
|
error!("{}", e);
|
|
}
|
|
|
|
Flash::success(
|
|
Redirect::to(uri!(reset)),
|
|
format!("Email was sent to {}", email.email),
|
|
)
|
|
}
|
|
|
|
#[get("/reset/<key>")]
|
|
fn reset_key(keys: State<Keys>, key: String, flash: Option<FlashMessage>) -> Option<Template> {
|
|
let keys = Arc::clone(&keys.keys);
|
|
if let Ok(keys) = keys.lock() {
|
|
let mut context = HashMap::new();
|
|
|
|
if let Some(ref msg) = flash {
|
|
context.insert("flash", msg.msg());
|
|
if msg.name() == "error" {
|
|
context.insert("flash_type", "Error");
|
|
}
|
|
}
|
|
|
|
if keys.contains_key(&key) {
|
|
context.insert("key", &key);
|
|
return Some(Template::render("reset_key", &context));
|
|
}
|
|
} else {
|
|
error!("Could not aquire lock for keys");
|
|
}
|
|
None
|
|
}
|
|
|
|
#[post("/reset/<key>", data = "<passwords>")]
|
|
fn reset_password(
|
|
config: State<Ldap0rConfig>,
|
|
keys: State<Keys>,
|
|
key: String,
|
|
passwords: Form<PasswordsForm>,
|
|
) -> Flash<Redirect> {
|
|
match set_password(&config, &keys, &key, &passwords) {
|
|
Ok(flash) => flash,
|
|
Err(e) => {
|
|
error!("{}", e);
|
|
Flash::error(
|
|
Redirect::to(uri!(reset_key: key)),
|
|
"Setting new password failed",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
rocket::ignite()
|
|
.mount(
|
|
"/",
|
|
routes![index, reset, reset_email, reset_key, reset_password],
|
|
)
|
|
.attach(AdHoc::on_attach("LDAP config", |rocket| {
|
|
let ldap0r_config = Ldap0rConfig {
|
|
domain: rocket
|
|
.config()
|
|
.get_str("domain")
|
|
.unwrap_or("https://localhost")
|
|
.to_string(),
|
|
ldap: LdapConfig {
|
|
server: rocket
|
|
.config()
|
|
.get_str("ldap_server")
|
|
.unwrap_or("ldap://localhost:2389")
|
|
.to_string(),
|
|
base: rocket
|
|
.config()
|
|
.get_str("ldap_base")
|
|
.unwrap_or("ou=Places,dc=example,dc=org")
|
|
.to_string(),
|
|
filter: rocket
|
|
.config()
|
|
.get_str("ldap_filter")
|
|
.unwrap_or("(objectClass=person)")
|
|
.to_string(),
|
|
bind: rocket
|
|
.config()
|
|
.get_str("ldap_bind")
|
|
.unwrap_or("cn=admin,dc=example,dc=org")
|
|
.to_string(),
|
|
password: rocket
|
|
.config()
|
|
.get_str("ldap_password")
|
|
.unwrap_or("secure")
|
|
.to_string(),
|
|
},
|
|
};
|
|
|
|
Ok(rocket.manage(ldap0r_config))
|
|
}))
|
|
.attach(Template::fairing())
|
|
.manage(Keys {
|
|
keys: Arc::new(Mutex::new(HashMap::new())),
|
|
})
|
|
.launch();
|
|
}
|