ldap0r/src/main.rs
finga 7b0e4b4a31 Password reset functionality
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
2020-07-06 15:30:45 +02:00

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();
}