fix and improve all login and password-related workflows

This commit is contained in:
glyph 2022-03-04 10:53:49 +02:00
parent 10049f0bc6
commit 7fdf88eaa8
14 changed files with 169 additions and 163 deletions

View File

@ -5,9 +5,12 @@ authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2018"
[dependencies]
async-std = "1.10.0"
chrono = "0.4.19"
dirs = "4.0"
fslock="0.1.6"
#golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi" }
golgi = { path = "../../../playground/rust/golgi" }
jsonrpc-client-core = "0.5"
jsonrpc-client-http = "0.5"
jsonrpc-core = "8.0.1"

23
peach-lib/issues_to_fix Normal file
View File

@ -0,0 +1,23 @@
- permissions for /var/lib/peachcloud
- everything fails if we don't have write permissions
- can't write config
- configure admin (add new admin)
- isn't persisted to config.yml
- on second try, it is persisted
- password reset via ssb pm is now working
- it sends a temporary password but says nothing about username
- what is the default username?
- why do we even have a username?
- login with temporary password fails
- "Invalid password: Password error: hash value in YAML configuration file is empty."
- things are generally working now :)
- for all form inputs:
- use a proper label (not just a placeholder)
- login

View File

@ -1,12 +1,14 @@
//! Interfaces for writing and reading PeachCloud configurations, stored in yaml.
//!
//! Different PeachCloud microservices import peach-lib, so that they can share this interface.
//! Different PeachCloud microservices import peach-lib, so that they can share
//! this interface.
//!
//! The configuration file is located at: "/var/lib/peachcloud/config.yml"
use std::fs;
use fslock::LockFile;
use log::debug;
use serde::{Deserialize, Serialize};
use crate::error::PeachError;
@ -72,6 +74,7 @@ pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
let peach_config_exists = std::path::Path::new(YAML_PATH).exists();
let peach_config: PeachConfig = if !peach_config_exists {
debug!("Loading peach config: {} does not exist", YAML_PATH);
PeachConfig {
external_domain: "".to_string(),
dyn_domain: "".to_string(),
@ -87,6 +90,7 @@ pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
}
// otherwise we load peach config from disk
else {
debug!("Loading peach config: {} exists", YAML_PATH);
let contents = fs::read_to_string(YAML_PATH).map_err(|source| PeachError::Read {
source,
path: YAML_PATH.to_string(),
@ -177,6 +181,7 @@ pub fn set_admin_password_hash(password_hash: &str) -> Result<PeachConfig, Peach
pub fn get_admin_password_hash() -> Result<String, PeachError> {
let peach_config = load_peach_config()?;
debug!("Admin password hash: {}", peach_config.admin_password_hash);
if !peach_config.admin_password_hash.is_empty() {
Ok(peach_config.admin_password_hash)
} else {

View File

@ -61,11 +61,8 @@ pub enum PeachError {
/// Represents a failure to parse or compile a regular expression.
Regex(regex::Error),
/// Represents a failure to successfully execute an sbot command.
SbotCli {
/// The `stderr` output from the sbot command.
msg: String,
},
/// Represents a failure to successfully execute an sbot command (via golgi).
Sbot(String),
/// Represents a failure to serialize or deserialize JSON.
SerdeJson(serde_json::error::Error),
@ -117,7 +114,7 @@ impl std::error::Error for PeachError {
PeachError::PasswordNotSet => None,
PeachError::Read { ref source, .. } => Some(source),
PeachError::Regex(_) => None,
PeachError::SbotCli { .. } => None,
PeachError::Sbot(_) => None,
PeachError::SerdeJson(_) => None,
PeachError::SerdeYaml(_) => None,
PeachError::SsbAdminIdNotFound { .. } => None,
@ -153,22 +150,19 @@ impl std::fmt::Display for PeachError {
write!(f, "Date/time parse error: {}", path)
}
PeachError::PasswordIncorrect => {
write!(f, "Password error: user-supplied password is incorrect")
write!(f, "password is incorrect")
}
PeachError::PasswordMismatch => {
write!(f, "Password error: user-supplied passwords do not match")
write!(f, "passwords do not match")
}
PeachError::PasswordNotSet => {
write!(
f,
"Password error: hash value in YAML configuration file is empty"
)
write!(f, "hash value in YAML configuration file is empty")
}
PeachError::Read { ref path, .. } => {
write!(f, "Read error: {}", path)
}
PeachError::Regex(ref err) => err.fmt(f),
PeachError::SbotCli { ref msg } => {
PeachError::Sbot(ref msg) => {
write!(f, "Sbot error: {}", msg)
}
PeachError::SerdeJson(ref err) => err.fmt(f),

View File

@ -1,7 +1,10 @@
use async_std::task;
use golgi::Sbot;
use log::debug;
use nanorand::{Rng, WyRand};
use sha3::{Digest, Sha3_256};
use crate::{config_manager, error::PeachError};
use crate::{config_manager, error::PeachError, sbot::SbotConfig};
/// Returns Ok(()) if the supplied password is correct,
/// and returns Err if the supplied password is incorrect.
@ -102,8 +105,37 @@ using this link: http://peach.local/reset_password",
// finally send the message to the admins
let peach_config = config_manager::load_peach_config()?;
for ssb_admin_id in peach_config.ssb_admin_ids {
// TODO: replace with golgi
//sbot_client::private_message(&msg, &ssb_admin_id)?;
// use golgi to send a private message on scuttlebutt
match task::block_on(publish_private_msg(&msg, &ssb_admin_id)) {
Ok(_) => (),
Err(e) => return Err(PeachError::Sbot(e)),
}
}
Ok(())
}
async fn publish_private_msg(msg: &str, recipient: &str) -> Result<(), String> {
// retrieve latest go-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
let msg = msg.to_string();
let recipient = vec![recipient.to_string()];
// initialise sbot connection with ip:port and shscap from config file
let mut sbot_client = match sbot_config {
// TODO: panics if we pass `Some(conf.shscap)` as second arg
Some(conf) => {
let ip_port = conf.lis.clone();
Sbot::init(Some(ip_port), None)
.await
.map_err(|e| e.to_string())?
}
None => Sbot::init(None, None).await.map_err(|e| e.to_string())?,
};
debug!("Publishing a Scuttlebutt private message with temporary password");
match sbot_client.publish_private(msg, recipient).await {
Ok(_) => Ok(()),
Err(e) => Err(format!("Failed to publish private message: {}", e)),
}
}

View File

@ -31,9 +31,6 @@ pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
login,
login_post,
logout,
reboot_cmd,
shutdown_cmd,
power_menu,
settings_menu,
set_theme,
],
@ -43,7 +40,6 @@ pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
routes![
admin_menu,
configure_admin,
add_admin,
add_admin_post,
delete_admin_post,
change_password,
@ -101,6 +97,7 @@ pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
/// required to run a complete PeachCloud build.
pub fn mount_peachcloud_routes(rocket: Rocket<Build>) -> Rocket<Build> {
mount_peachpub_routes(rocket)
.mount("/", routes![reboot_cmd, shutdown_cmd, power_menu,])
.mount(
"/settings/network",
routes![

View File

@ -6,7 +6,6 @@ use rocket::{
post,
request::{self, FlashMessage, FromRequest, Request},
response::{Flash, Redirect},
serde::Deserialize,
};
use rocket_dyn_templates::{tera::Context, Template};
@ -89,17 +88,14 @@ pub fn login(flash: Option<FlashMessage>) -> Template {
Template::render("login", &context.into_json())
}
#[derive(Debug, Deserialize, FromForm)]
#[derive(Debug, FromForm)]
pub struct LoginForm {
pub username: String,
pub password: String,
}
/// Takes in a LoginForm and returns Ok(()) if username and password
/// are correct to authenticate with peach-web.
/// Takes in a LoginForm and returns Ok(()) if the password is correct.
///
/// Note: currently there is only one user, and the username should always
/// be "admin".
/// Note: there is currently only one user, therefore we don't need a username.
pub fn verify_login_form(login_form: LoginForm) -> Result<(), PeachError> {
password_utils::verify_password(&login_form.password)
}
@ -117,13 +113,14 @@ pub fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) -> Templ
TemplateOrRedirect::Redirect(Redirect::to("/"))
}
Err(_) => {
Err(e) => {
let err_msg = format!("Invalid password: {}", e);
// if unsuccessful login, render /login page again
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Login".to_string()));
context.insert("flash_name", &("error".to_string()));
context.insert("flash_msg", &("Invalid password".to_string()));
context.insert("flash_msg", &(err_msg));
TemplateOrRedirect::Template(Template::render("login", &context.into_json()))
}
@ -142,7 +139,7 @@ pub fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
// HELPERS AND ROUTES FOR /reset_password
#[derive(Debug, Deserialize, FromForm)]
#[derive(Debug, FromForm)]
pub struct ResetPasswordForm {
pub temporary_password: String,
pub new_password1: String,
@ -198,7 +195,7 @@ pub fn reset_password_post(reset_password_form: Form<ResetPasswordForm>) -> Temp
let (flash_name, flash_msg) = match save_reset_password_form(reset_password_form.into_inner()) {
Ok(_) => (
"success".to_string(),
"New password is now saved. Return home to login".to_string(),
"New password has been saved. Return home to login".to_string(),
),
Err(err) => (
"error".to_string(),
@ -266,9 +263,9 @@ pub fn send_password_reset_post() -> Template {
// HELPERS AND ROUTES FOR /settings/change_password
#[derive(Debug, Deserialize, FromForm)]
#[derive(Debug, FromForm)]
pub struct PasswordForm {
pub old_password: String,
pub current_password: String,
pub new_password1: String,
pub new_password2: String,
}
@ -277,9 +274,9 @@ pub struct PasswordForm {
pub fn save_password_form(password_form: PasswordForm) -> Result<(), PeachWebError> {
info!(
"change password!: {} {} {}",
password_form.old_password, password_form.new_password1, password_form.new_password2
password_form.current_password, password_form.new_password1, password_form.new_password2
);
password_utils::verify_password(&password_form.old_password)?;
password_utils::verify_password(&password_form.current_password)?;
// if the previous line did not throw an error, then the old password is correct
password_utils::validate_new_passwords(
&password_form.new_password1,
@ -321,7 +318,7 @@ pub fn change_password_post(password_form: Form<PasswordForm>, _auth: Authentica
let (flash_name, flash_msg) = match save_password_form(password_form.into_inner()) {
Ok(_) => (
"success".to_string(),
"New password is now saved".to_string(),
"New password has been saved".to_string(),
),
Err(err) => (
"error".to_string(),

View File

@ -3,7 +3,6 @@ use rocket::{
get, post,
request::FlashMessage,
response::{Flash, Redirect},
serde::Deserialize,
uri,
};
use rocket_dyn_templates::{tera::Context, Template};
@ -77,50 +76,31 @@ pub fn configure_admin(flash: Option<FlashMessage>, _auth: Authenticated) -> Tem
// HELPERS AND ROUTES FOR /settings/admin/add
#[derive(Debug, Deserialize, FromForm)]
#[derive(Debug, FromForm)]
pub struct AddAdminForm {
pub ssb_id: String,
}
pub fn save_add_admin_form(admin_form: AddAdminForm) -> Result<(), PeachWebError> {
let _result = config_manager::add_ssb_admin_id(&admin_form.ssb_id)?;
// if the previous line didn't throw an error then it was a success
Ok(())
}
#[get("/add")]
pub fn add_admin(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
// retrieve current ui theme
let theme = utils::get_theme();
let mut context = Context::new();
context.insert("theme", &theme);
context.insert("back", &Some("/settings/admin/configure".to_string()));
context.insert("title", &Some("Add Admin".to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
// template_dir is set in Rocket.toml
Template::render("settings/admin/add_admin", &context.into_json())
}
#[post("/add", data = "<add_admin_form>")]
pub fn add_admin_post(add_admin_form: Form<AddAdminForm>, _auth: Authenticated) -> Flash<Redirect> {
let result = save_add_admin_form(add_admin_form.into_inner());
let url = uri!("/settings/admin/configure");
match result {
Ok(_) => Flash::success(Redirect::to(url), "Successfully added new admin"),
Err(_) => Flash::error(Redirect::to(url), "Failed to add new admin"),
Ok(_) => Flash::success(Redirect::to(url), "Added SSB administrator"),
Err(e) => Flash::error(Redirect::to(url), format!("Failed to add new admin: {}", e)),
}
}
// HELPERS AND ROUTES FOR /settings/admin/delete
#[derive(Debug, Deserialize, FromForm)]
#[derive(Debug, FromForm)]
pub struct DeleteAdminForm {
pub ssb_id: String,
}
@ -131,9 +111,12 @@ pub fn delete_admin_post(
_auth: Authenticated,
) -> Flash<Redirect> {
let result = config_manager::delete_ssb_admin_id(&delete_admin_form.ssb_id);
let url = uri!(configure_admin);
let url = uri!("/settings/admin", configure_admin);
match result {
Ok(_) => Flash::success(Redirect::to(url), "Successfully removed admin id"),
Err(_) => Flash::error(Redirect::to(url), "Failed to remove admin id"),
Ok(_) => Flash::success(Redirect::to(url), "Removed SSB administrator"),
Err(e) => Flash::error(
Redirect::to(url),
format!("Failed to remove admin id: {}", e),
),
}
}

View File

@ -2,22 +2,19 @@
{%- block card %}
<!-- LOGIN FORM -->
<div class="card center">
<div class="card-container">
<form id="login_form" class="center" action="/login" method="post">
<!-- input for username -->
<input id="username" name="username" class="center input" type="text" placeholder="Username" title="Username for authentication" autofocus/>
<form id="login_form" class="center" action="/login" method="post">
<div style="display: flex; flex-direction: column; margin-bottom: 1rem;">
<!-- input for password -->
<input id="password" name="password" class="center input" type="password" placeholder="Password" title="Password for given username"/>
<div id="buttonDiv">
<input id="loginUser" class="button button-primary center" title="Login" type="submit" value="Login">
<label for="password" class="center label-small font-gray" style="width: 80%;">PASSWORD</label>
<input id="password" name="password" class="center input" type="password" title="Password for given username"/>
<!-- login (form submission) button -->
<input id="loginUser" class="button button-primary center" title="Login" type="submit" value="Login">
<div class="center-text" style="margin-top: 1rem;">
<a href="/settings/admin/forgot_password" class="label-small link font-gray">Forgot Password?</a>
</div>
</form>
</div>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<div class="center-text" style="margin-top: 25px;">
<a href="/settings/admin/forgot_password" class="label-small link font-gray">Forgot Password?</a>
</div>
</div>
</form>
</div>
{%- endblock card -%}

View File

@ -1,17 +0,0 @@
{%- extends "nav" -%}
{%- block card %}
<!-- ADD ADMIN FORM -->
<div class="card center">
<div class="card-container">
<form id="addAdminForm" action="/settings/admin/add" method="post">
<input id="ssb_id" name="ssb_id" class="center input" type="text" placeholder="SSB ID" title="SSB ID of Admin" value=""/>
<div id="buttonDiv">
<input id="addAdmin" class="button button-primary center" title="Add" type="submit" value="Add">
<a class="button button-secondary center" href="/settings/admin/configure" title="Cancel">Cancel</a>
</div>
</form>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</div>
</div>
{%- endblock card -%}

View File

@ -3,13 +3,17 @@
<!-- CHANGE PASSWORD FORM -->
<div class="card center">
<form id="changePassword" class="center" action="/settings/admin/change_password" method="post">
<!-- input for current password -->
<input id="currentPassword" class="center input" name="current_password" type="password" placeholder="Current password" title="Current password" autofocus>
<!-- input for new password -->
<input id="newPassword" class="center input" name="new_password1" type="password" placeholder="New password" title="New password">
<!-- input for duplicate new password -->
<input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" placeholder="Re-enter new password" title="New password duplicate">
<div id="buttonDiv">
<div style="display: flex; flex-direction: column; margin-bottom: 1rem;">
<!-- input for current password -->
<label for="currentPassword" class="center label-small font-gray" style="width: 80%;">CURRENT PASSWORD</label>
<input id="currentPassword" class="center input" name="current_password" type="password" title="Current password" autofocus>
<!-- input for new password -->
<label for="newPassword" class="center label-small font-gray" style="width: 80%;">NEW PASSWORD</label>
<input id="newPassword" class="center input" name="new_password1" type="password" title="New password">
<!-- input for duplicate new password -->
<label for="newPasswordDuplicate" class="center label-small font-gray" style="width: 80%;">RE-ENTER NEW PASSWORD</label>
<input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" title="New password duplicate">
<!-- save (form submission) button -->
<input id="savePassword" class="button button-primary center" title="Add" type="submit" value="Save">
<a class="button button-secondary center" href="/settings/admin" title="Cancel">Cancel</a>
</div>

View File

@ -2,25 +2,31 @@
{%- block card %}
<!-- CONFIGURE ADMIN PAGE -->
<div class="card center">
<div class="text-container">
<h4 class="font-normal">Current Admins</h4>
{% if not ssb_admin_ids %}
<div class="card-text">
There are no currently configured admins.
</div>
{% else %}
{% for admin in ssb_admin_ids %}
<div>
<form action="/settings/admin/delete" method="post">
<input type="hidden" name="ssb_id" value="{{admin}}"/>
<input type="submit" value="X" title="Delete"/> <span>{{ admin }}</span>
</form>
<div class="capsule capsule-profile center-text font-normal border-info" style="font-family: var(--sans-serif); font-size: var(--font-size-6); margin-bottom: 1.5rem;">Administrators are identified and added by their Scuttlebutt public keys. These accounts will be sent private messages on Scuttlebutt when a password reset is requested.</div>
{% if not ssb_admin_ids %}
<div class="card-text">
There are no currently configured admins.
</div>
{% else %}
{% for admin in ssb_admin_ids %}
<form class="center" action="/settings/admin/delete" method="post">
<div class="center" style="display: flex; justify-content: space-between;">
<input type="hidden" name="ssb_id" value="{{ admin }}"/>
<p class="label-small label-ellipsis font-gray" style="user-select: all;">{{ admin }}</p>
<input style="width: 30%;" type="submit" class="button button-warning" value="Delete" title="Delete SSB administrator"/>
</div>
{% endfor %}
{% endif %}
<a class="button button-primary center full-width" style="margin-top: 25px;" href="/settings/admin/add" title="Add Admin">Add Admin</a>
</div>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</form>
{% endfor %}
{% endif %}
<form id="addAdmin" class="center" style="margin-top: 2rem;" action="/settings/admin/add" method="post">
<div class="center" style="display: flex; flex-direction: column; margin-bottom: 2rem;" title="Public key (ID) of a desired administrator">
<label for="publicKey" class="label-small font-gray">PUBLIC KEY</label>
<input type="text" id="publicKey" name="ssb_id" placeholder="@xYz...=.ed25519" autofocus>
</div>
<!-- BUTTONS -->
<input class="button button-primary center" type="submit" title="Add SSB administrator" value="Add Admin">
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</form>
</div>
{%- endblock card -%}

View File

@ -2,14 +2,15 @@
{%- block card %}
<!-- PASSWORD RESET REQUEST CARD -->
<div class="card center">
<div class="capsule capsule-container info-border">
<p class="card-text">Click the button below to send a new temporary password which can be used to change your device password.
</br></br>
The temporary password will be sent in an SSB private message to the admin of this device.</p>
<div class="capsule capsule-container border-info">
<p class="card-text">Click the 'Send Password Reset' button to send a new temporary password which can be used to change your device password.</p>
<p class="card-text" style="margin-top: 1rem;">The temporary password will be sent in an SSB private message to the admin of this device.</p>
<p class="card-text" style="margin-top: 1rem;">Once you have the temporary password, click the 'Set New Password' button to reach the password reset page.</p>
</div>
<form id="sendPasswordReset" action="/send_password_reset" method="post">
<form id="sendPasswordReset" action="/settings/admin/send_password_reset" method="post">
<div id="buttonDiv">
<input class="button button-primary center" style="margin-top: 1rem;" type="submit" value="Send Password Reset" title="Send Password Reset Link"/>
<input class="button button-primary center" style="margin-top: 1rem;" type="submit" value="Send Password Reset" title="Send password reset link"/>
<a href="/settings/admin/reset_password" class="button button-primary center" title="Set a new password using the temporary password">Set New Password</a>
</div>
</form>
<!-- FLASH MESSAGE -->

View File

@ -2,41 +2,22 @@
{%- block card %}
<!-- RESET PASSWORD PAGE -->
<div class="card center">
<div class="form-container">
<form id="changePassword" action="/reset_password" method="post">
<div class="input-wrapper">
<!-- input for temporary password -->
<label id="temporary_password" class="label-small input-label font-near-black">
<label class="label-small input-label font-gray" for="temporary_password" style="padding-top: 0.25rem;">Temporary Password</label>
<input id="temporary_password" class="form-input" style="margin-bottom: 0;"
name="temporary_password" type="password" title="temporary password" value="">
</label>
</div>
<div class="input-wrapper">
<!-- input for new password1 -->
<label id="new_password1" class="label-small input-label font-near-black">
<label class="label-small input-label font-gray" for="new_password1" style="padding-top: 0.25rem;">Enter New Password</label>
<input id="new_password1" class="form-input" style="margin-bottom: 0;"
name="new_password1" title="new_password1" type="password" value="">
</label>
</div>
<div class="input-wrapper">
<!-- input for new password2 -->
<label id="new_password2" class="label-small input-label font-near-black">
<label class="label-small input-label font-gray" for="new_password2" style="padding-top: 0.25rem;">Re-Enter New Password</label>
<input id="new_password2" class="form-input" style="margin-bottom: 0;"
name="new_password2" title="new_password2" type="password" value="">
</label>
</div>
<div id="buttonDiv">
<input id="changePasswordButton" class="button button-primary center" title="Add" type="submit" value="Save">
</div>
</form>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</div>
<form id="changePassword" class="center" action="/settings/admin/reset_password" method="post">
<div style="display: flex; flex-direction: column; margin-bottom: 1rem;">
<!-- input for temporary password -->
<label class="center label-small font-gray" style="width: 80%;" for="temporary_password">TEMPORARY PASSWORD</label>
<input id="temporary_password" class="center input" name="temporary_password" type="password" title="temporary password" value="">
<!-- input for new password1 -->
<label class="center label-small font-gray" style="width: 80%;" for="new_password1">NEW PASSWORD</label>
<input id="new_password1" class="center input" name="new_password1" title="new_password1" type="password" value="">
<!-- input for new password2 -->
<label class="center label-small font-gray" style="width: 80%;" for="new_password2">RE-ENTER NEW PASSWORD</label>
<input id="new_password2" class="center input" name="new_password2" title="new_password2" type="password" value="">
<!-- save (form submission) button -->
<input id="changePasswordButton" class="button button-primary center" title="Add" type="submit" value="Save">
</div>
</form>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</div>
{%- endblock card -%}