Initial commit
This commit is contained in:
74
src/api.rs
Normal file
74
src/api.rs
Normal 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
56
src/config.rs
Normal 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
4
src/exit.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub fn exit_with_error(message: &str) -> ! {
|
||||
eprintln!("{}", message);
|
||||
std::process::exit(-1)
|
||||
}
|
62
src/main.rs
Normal file
62
src/main.rs
Normal 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
46
src/static.rs
Normal 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
15
src/static/checkmark.svg
Normal 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
22
src/static/cross.svg
Normal 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
18
src/static/hourglass.svg
Normal 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 |
18
src/static/hourglass_backup.svg
Normal file
18
src/static/hourglass_backup.svg
Normal 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
28
src/static/index.html
Normal 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
11
src/static/legal.html
Normal 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>
|
105
src/static/webldappasswd.css
Normal file
105
src/static/webldappasswd.css
Normal 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;
|
||||
}
|
78
src/static/webldappasswd.js
Normal file
78
src/static/webldappasswd.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user