Initial commit

This commit is contained in:
2021-02-10 14:49:30 +01:00
commit 1428d07ee9
20 changed files with 2690 additions and 0 deletions

74
src/api.rs Normal file
View File

@ -0,0 +1,74 @@
use ldap3::LdapConn;
use ldap3::exop::PasswordModify;
use ldap3::result::{LdapError, Result};
use rocket_contrib::json::Json;
use crate::config::Config;
#[derive(Deserialize)]
pub struct PasswordData {
username: String,
old_password: String,
new_password: String,
}
#[derive(Serialize)]
enum Message {
InvalidCredentials,
ServerError,
}
#[derive(Serialize)]
pub struct Response {
success: bool,
message: Option<Message>,
}
fn change_password(data: Json<PasswordData>, config: rocket::State<Config>) -> Result<()> {
let dn = format!("uid={},ou=People,dc=fiveop,dc=de", &data.username);
let mut ldap = LdapConn::new(&config.ldap_url)?;
ldap
.simple_bind(&dn, &data.old_password)?
.success()?;
ldap
.extended(PasswordModify{
user_id: Some(&dn),
old_pass: Some(&data.old_password),
new_pass: Some(&data.new_password),
})?
.success()?;
Ok(())
}
#[post("/update", data = "<data>")]
pub fn update(data: Json<PasswordData>, config: rocket::State<Config>) -> Json<Response> {
Json(
match change_password(data, config) {
Ok(_) => Response{
success: true,
message: None,
},
Err(error) => {
eprintln!("LDAP error: {}", error);
Response {
success: false,
message: Some(
match error {
LdapError::LdapResult{ result } => {
if result.rc == 49 {
Message::InvalidCredentials
} else {
Message::ServerError
}
},
_ => Message::ServerError,
}
),
}
},
}
)
}

56
src/config.rs Normal file
View File

@ -0,0 +1,56 @@
use std::io::BufReader;
use std::fs::File;
use handlebars::Handlebars;
use crate::exit::exit_with_error;
fn default_ldap_url() -> String {
"ldap://localhost".to_string()
}
fn default_host() -> String {
"localhost".to_string()
}
fn default_port() -> u16 {
8000
}
#[derive(Deserialize)]
pub struct Config {
pub dn: String,
#[serde(default = "default_ldap_url")]
pub ldap_url: String,
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
}
pub fn check_config(config: Config) -> Config {
let hb = Handlebars::new();
if let Some(e) = hb.render_template(&config.dn, &json!({"username" : "foo"})).err() {
exit_with_error(&format!("Failed to parse 'dn' ({}) in configuration file: {}", config.dn, e))
};
config
}
pub fn load_config(file_path: &str) -> Config {
let file = match File::open(file_path) {
Ok(file) => file,
Err(_) => exit_with_error(&format!("Failed to open configuration file '{}'", file_path))
};
let reader = BufReader::new(file);
match serde_json::from_reader::<BufReader<_>, Config>(reader) {
Ok(config) => check_config(config),
Err(_) => exit_with_error(&format!("Failed to parse configuration file '{}'", file_path))
}
}

4
src/exit.rs Normal file
View File

@ -0,0 +1,4 @@
pub fn exit_with_error(message: &str) -> ! {
eprintln!("{}", message);
std::process::exit(-1)
}

62
src/main.rs Normal file
View File

@ -0,0 +1,62 @@
#![feature(proc_macro_hygiene, decl_macro)]
extern crate clap;
extern crate handlebars;
extern crate ldap3;
#[macro_use] extern crate rocket;
extern crate rocket_contrib;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate serde_json;
mod api;
mod config;
mod exit;
mod r#static;
use clap::{Arg, App};
use rocket::config::{Config, Environment};
use crate::config::load_config;
use crate::exit::exit_with_error;
const VERSION: &str = "1.0";
const DEFAULT_CONFIG_FILE_PATH: &str = "/etc/webldappasswd/config.json";
fn main() {
let matches = App::new("WebLDAPPasswd")
.version(VERSION)
.arg(Arg::with_name("config")
.short("c")
.long("config")
.value_name("CONFIG_FILE_PATH")
.help("Custom configuration file")
.takes_value(true))
.get_matches();
let config_file_path = matches
.value_of("config")
.unwrap_or(DEFAULT_CONFIG_FILE_PATH);
let config = load_config(config_file_path);
let rocket_config_builder = Config::build(Environment::Production)
.address(&config.host)
.port(config.port);
let rocket_config = match rocket_config_builder.finalize() {
Ok(config) => config,
Err(e) => exit_with_error(&format!("Bad host address ({}) in configuration file: {}", config.host, e)),
};
rocket::custom(rocket_config)
.mount("/", routes![r#static::index,
r#static::legal,
r#static::css,
r#static::js,
r#static::checkmark,
r#static::cross,
r#static::hourglass,
api::update])
.manage(config)
.launch();
}

46
src/static.rs Normal file
View File

@ -0,0 +1,46 @@
use rocket::http::{ContentType, Status};
use rocket::request::Request;
use rocket::response::{content, Responder, Response};
pub struct Svg<R>(pub R);
impl<'r, R: Responder<'r>> Responder<'r> for Svg<R> {
fn respond_to(self, req: &Request) -> Result<Response<'r>, Status> {
content::Content(ContentType::SVG, self.0).respond_to(req)
}
}
#[get("/")]
pub fn index() -> content::Html<&'static str> {
content::Html(include_str!("static/index.html"))
}
#[get("/checkmark.svg")]
pub fn checkmark() -> Svg<&'static str> {
Svg(include_str!("static/checkmark.svg"))
}
#[get("/cross.svg")]
pub fn cross() -> Svg<&'static str> {
Svg(include_str!("static/cross.svg"))
}
#[get("/hourglass.svg")]
pub fn hourglass() -> Svg<&'static str> {
Svg(include_str!("static/hourglass.svg"))
}
#[get("/legal.html")]
pub fn legal() -> content::Html<&'static str> {
content::Html(include_str!("static/legal.html"))
}
#[get("/webldappasswd.css")]
pub fn css() -> content::Css<&'static str> {
content::Css(include_str!("static/webldappasswd.css"))
}
#[get("/webldappasswd.js")]
pub fn js() -> content::JavaScript<&'static str> {
content::JavaScript(include_str!("static/webldappasswd.js"))
}

15
src/static/checkmark.svg Normal file
View File

@ -0,0 +1,15 @@
<svg width="100"
height="100"
viewBox="-1 -1 2 2"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<path stroke-width="0.2"
stroke="black"
fill="none"
d="
M -0.724 0.181
L -0.362 0.543
L 0 0.181
L 0.362 -0.181
L 0.724 -0.543"/>
</svg>

After

Width:  |  Height:  |  Size: 376 B

22
src/static/cross.svg Normal file
View File

@ -0,0 +1,22 @@
<svg width="100"
height="100"
viewBox="-1 -1 2 2"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<path stroke="none"
fill="black"
transform="rotate(45)"
d="M 0.1 0.9
L 0.1 0.1
L 0.9 0.1
L 0.9 -0.1
L 0.1 -0.1
L 0.1 -0.9
L -0.1 -0.9
L -0.1 -0.1
L -0.9 -0.1
L -0.9 0.1
L -0.1 0.1
L -0.1 0.9
Z"/>
</svg>

After

Width:  |  Height:  |  Size: 522 B

18
src/static/hourglass.svg Normal file
View File

@ -0,0 +1,18 @@
<svg width="100"
height="100"
viewBox="-1 -1 2 2"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<path stroke-width="0.2"
stroke="black"
stroke-linejoin="round"
fill="none"
d="
M 0 -0.536
L 0.536 -0.536
L 0 0
L -0.536 0.536
L 0.536 0.536
L -0.536 -0.536
L 0 -0.536"/>
</svg>

After

Width:  |  Height:  |  Size: 458 B

View File

@ -0,0 +1,18 @@
<svg width="100"
height="100"
viewBox="-1 -1 2 2"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<path stroke-width="0.2"
stroke="black"
stroke-linejoin="round"
fill="none"
d="
M 0 -0.705
L 0.303 -0.705
L 0 0
L -0.303 0.705
L 0.303 0.705
L -0.303 -0.705
L 0 -0.705"/>
</svg>

After

Width:  |  Height:  |  Size: 458 B

28
src/static/index.html Normal file
View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebLDAPPasswd</title>
<link rel="stylesheet" href="webldappasswd.css">
<script src="webldappasswd.js"></script>
</head>
<body>
<form>
<label>Username</label>
<input type="text" name="username" autocomplete="username">
<label>Old Password</label>
<input type="password" name="old_password" autocomplete="current-password">
<label>New Password</label>
<input type="password" name="new_password" autocomplete="new-password">
<button type="button" onclick="changePasswords()">Change Password</button>
</form>
<nav>
<a href="legal.html">Legal Notes</a>
<a href="https://git.schaefer-wolke.de/fiveop/webldappasswd/">WebLDAPPasswd</a>
</nav>
<div id="modal">
<span id="message"><img src="hourglass.svg"> Waiting for server response ...</span>
<button id="close_button" type="button" disabled onClick="hideModal()">Close</button>
</div>
</body>
</html>

11
src/static/legal.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebLDAPPasswd</title>
</head>
<script src="webldappasswd.js"></script>
<body>
The administrator of this WebLDAPPasswd instance is lazy and should feel bad.
</body>
</html>

View File

@ -0,0 +1,105 @@
* {
box-sizing: border-box;
}
body {
margin: 0px;
display: grid;
grid-template-columns: 1fr;
justify-items: center;
font-size: 18pt;
font-family: sans-serif;
}
form {
width: 720px;
display: grid;
grid-template-columns: 1fr;
}
input {
height: 2em;
padding: 0.25em 0.5em;
border: 2px solid black;
background-color: #fff;
font-family: inherit;
font-size: 18pt;
line-height: 1;
}
button:focus, button:hover, input:focus, input:hover {
box-shadow: 0px 0px 0px 2px #707070;
}
button:focus, input:focus {
border-color: #000000;
outline: 3px solid transparent;
}
button {
height: 2em;
padding: 0.25em 0.5em;
justify-self: center;
background-color: #fff;
border: 2px solid black;
font-family: inherit;
font-size: 18pt;
line-height: 1;
}
button:active {
transform: scale(0.9);
}
button:disabled {
color: #707070;
transform: scale(1);
box-shadow: none;
outline: none;
border-color: #707070;
}
label, button, a {
margin-top: 1em;
margin-bottom: 0.2em;
}
a {
color: black;
font-size: 80%;
}
#modal {
display: none;
margin: 0px;
position: absolute;
z-index: 32767;
background: white;
height: 100%;
width: 100%;
border: 10px solid rgba(0, 0, 0, 0.5);
flex-direction: column;
justify-content: center;
align-items: center;
}
#modal span {
display: flex;
align-items: center;
}
#modal span img {
height: 2em;
width: 2em;
}

View File

@ -0,0 +1,78 @@
MODAL_STATES = {
"Wait" : {
button_disabled : true,
message : "Waiting for server response ...",
icon_url : "hourglass.svg",
},
"Success" : {
button_disabled : false,
message : "Password changed",
icon_url : "checkmark.svg",
},
"FetchError" : {
button_disabled : false,
message : function(parameters) { `An error occurred while contacting the server ({parameters.statusCode})` },
},
"ServerError" : {
button_disabled : false,
message : "A server error occurred. Password was not changed.",
icon_url : "cross.svg",
},
"InvalidCredentials" : {
button_disabled : false,
message : "Invalid credentials provided. Password was not changed.",
icon_url : "cross.svg",
},
}
function showModal(state_name, parameters) {
let state = MODAL_STATES[state_name];
let message = document.getElementById("message");
let img = message.childNodes[0];
img.src = state.icon_url;
let text = message.childNodes[1];
text.textContent = " " + (state.message instanceof Function ? state.message(parameters) : state.message);
let button = document.getElementById("close_button");
button.disabled = state.button_disabled;
let modal = document.getElementById("modal");
modal.style.display = 'flex';
}
function hideModal() {
let modal = document.getElementById("modal");
modal.style.display = 'none';
}
FIELDS = ['username', 'old_password', 'new_password']
function changePasswords() {
let query = {};
for (field in FIELDS) {
query[FIELDS[field]] = document.querySelectorAll('input[name=' + FIELDS[field] + ']')[0].value;
}
showModal("Wait");
fetch("update", {
method: "POST",
body: JSON.stringify(query),
}).then(response => {
if(response.status == 200) {
return response.json();
} else {
showModal("FetchError", { statusCode : response.status });
}
}).then(response => {
if(response.success) {
showModal("Success");
} else {
showModal(response.message);
}
});
}