Improved error handling

Massive refactor of error handling to make messages easier to debug
This commit is contained in:
ViViDboarder 2021-06-13 18:01:23 -07:00
parent c090fb5a52
commit 77919aa3d4
5 changed files with 1012 additions and 755 deletions

1465
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,3 +10,5 @@ serde = { version = "1.0", features = ["derive"] }
toml = "0.5" toml = "0.5"
reqwest = "0.9" reqwest = "0.9"
serde_json = "1.0" serde_json = "1.0"
thiserror = "1.0"
anyhow = "1.0"

View File

@ -151,7 +151,7 @@ impl Config {
pub fn get_ldap_mail_field(&self) -> String { pub fn get_ldap_mail_field(&self) -> String {
match &self.ldap_mail_field { match &self.ldap_mail_field {
Some(mail_field) => mail_field.clone(), Some(mail_field) => mail_field.clone(),
None => String::from("mail").clone(), None => String::from("mail"),
} }
} }

View File

@ -1,10 +1,13 @@
extern crate anyhow;
extern crate ldap3; extern crate ldap3;
use std::collections::HashSet; use std::collections::HashSet;
use std::error::Error;
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
use anyhow::Context as _;
use anyhow::Error as AnyError;
use anyhow::Result;
use ldap3::{DerefAliases, LdapConn, LdapConnSettings, Scope, SearchEntry, SearchOptions}; use ldap3::{DerefAliases, LdapConn, LdapConnSettings, Scope, SearchEntry, SearchOptions};
mod config; mod config;
@ -13,34 +16,28 @@ mod vw_admin;
fn main() { fn main() {
let config = config::Config::from_file(); let config = config::Config::from_file();
let mut client = vw_admin::Client::new( let mut client = vw_admin::Client::new(
config.get_vaultwarden_url().clone(), config.get_vaultwarden_url(),
config.get_vaultwarden_admin_token().clone(), config.get_vaultwarden_admin_token(),
config.get_vaultwarden_root_cert_file().clone(), config.get_vaultwarden_root_cert_file(),
); );
if let Err(e) = invite_users(&config, &mut client, config.get_ldap_sync_loop()) { invite_users(&config, &mut client, config.get_ldap_sync_loop())
panic!("{}", e);
}
} }
/// Invites new users to Bitwarden from LDAP /// Invites new users to Bitwarden from LDAP
fn invite_users( fn invite_users(config: &config::Config, client: &mut vw_admin::Client, start_loop: bool) {
config: &config::Config,
client: &mut vw_admin::Client,
start_loop: bool,
) -> Result<(), Box<dyn Error>> {
if start_loop { if start_loop {
start_sync_loop(config, client)?; start_sync_loop(config, client).expect("Failed to start invite sync loop");
} else { } else {
invite_from_ldap(config, client)?; invite_from_ldap(config, client).expect("Failed to invite users");
} }
Ok(())
} }
/// Creates set of email addresses for users that already exist in Bitwarden /// Creates set of email addresses for users that already exist in Bitwarden
fn get_existing_users(client: &mut vw_admin::Client) -> Result<HashSet<String>, Box<dyn Error>> { fn get_existing_users(client: &mut vw_admin::Client) -> Result<HashSet<String>, AnyError> {
let all_users = client.users()?; let all_users = client
.users()
.context("Could not get list of existing users from server")?;
let mut user_emails = HashSet::with_capacity(all_users.len()); let mut user_emails = HashSet::with_capacity(all_users.len());
for user in all_users { for user in all_users {
user_emails.insert(user.get_email().to_lowercase()); user_emails.insert(user.get_email().to_lowercase());
@ -67,45 +64,44 @@ fn ldap_client(
bind_pw: String, bind_pw: String,
no_tls_verify: bool, no_tls_verify: bool,
starttls: bool, starttls: bool,
) -> Result<LdapConn, Box<dyn Error>> { ) -> Result<LdapConn, AnyError> {
let settings = LdapConnSettings::new() let settings = LdapConnSettings::new()
.set_starttls(starttls) .set_starttls(starttls)
.set_no_tls_verify(no_tls_verify); .set_no_tls_verify(no_tls_verify);
let ldap = LdapConn::with_settings(settings, ldap_url.as_str())?; let ldap = LdapConn::with_settings(settings, ldap_url.as_str())
match ldap.simple_bind(bind_dn.as_str(), bind_pw.as_str()) { .context("Failed to connect to LDAP server")?;
_ => {} ldap.simple_bind(bind_dn.as_str(), bind_pw.as_str())
}; .context("Could not bind to LDAP server")?;
Ok(ldap) Ok(ldap)
} }
/// Retrieves search results from ldap /// Retrieves search results from ldap
fn search_entries(config: &config::Config) -> Result<Vec<SearchEntry>, Box<dyn Error>> { fn search_entries(config: &config::Config) -> Result<Vec<SearchEntry>, AnyError> {
let ldap = ldap_client( let ldap = ldap_client(
config.get_ldap_url(), config.get_ldap_url(),
config.get_ldap_bind_dn(), config.get_ldap_bind_dn(),
config.get_ldap_bind_password(), config.get_ldap_bind_password(),
config.get_ldap_no_tls_verify(), config.get_ldap_no_tls_verify(),
config.get_ldap_starttls(), config.get_ldap_starttls(),
); )
.context("LDAP client initialization failed")?;
if ldap.is_err() {
println!("Error: Could not bind to ldap server");
}
let mail_field = config.get_ldap_mail_field(); let mail_field = config.get_ldap_mail_field();
let fields = vec!["uid", "givenname", "sn", "cn", mail_field.as_str()]; let fields = vec!["uid", "givenname", "sn", "cn", mail_field.as_str()];
// TODO: Something something error handling // TODO: Something something error handling
let (results, _res) = ldap? let (results, _res) = ldap
.with_search_options(SearchOptions::new().deref(DerefAliases::Always)) .with_search_options(SearchOptions::new().deref(DerefAliases::Always))
.search( .search(
&config.get_ldap_search_base_dn().as_str(), &config.get_ldap_search_base_dn().as_str(),
Scope::Subtree, Scope::Subtree,
&config.get_ldap_search_filter().as_str(), &config.get_ldap_search_filter().as_str(),
fields, fields,
)? )
.success()?; .context("LDAP search failure")?
.success()
.context("LDAP search usucessful")?;
// Build list of entries // Build list of entries
let mut entries = Vec::new(); let mut entries = Vec::new();
@ -120,56 +116,65 @@ fn search_entries(config: &config::Config) -> Result<Vec<SearchEntry>, Box<dyn E
fn invite_from_ldap( fn invite_from_ldap(
config: &config::Config, config: &config::Config,
client: &mut vw_admin::Client, client: &mut vw_admin::Client,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), AnyError> {
match get_existing_users(client) { let existing_users =
Ok(existing_users) => { get_existing_users(client).context("Failed to get existing users from server")?;
let mail_field = config.get_ldap_mail_field(); let mail_field = config.get_ldap_mail_field();
let mut num_users = 0; let mut num_users = 0;
for ldap_user in search_entries(config)? {
// Safely get first email from list of emails in field
if let Some(user_email) = ldap_user
.attrs
.get(mail_field.as_str())
.and_then(|l| (l.first()))
{
if existing_users.contains(&user_email.to_lowercase()) {
println!("User with email already exists: {}", user_email);
} else {
println!("Try to invite user: {}", user_email);
// TODO: Validate response
let _response = client.invite(user_email);
num_users = num_users + 1;
// println!("Invite response: {:?}", response);
}
} else {
match ldap_user.attrs.get("uid").and_then(|l| l.first()) {
Some(user_uid) =>
println!("Warning: Email field, {:?}, not found on user {}", mail_field, user_uid),
None => println!("Warning: Email field, {:?}, not found on user", mail_field)
}
}
}
// Maybe think about returning this value for some other use for ldap_user in search_entries(config)? {
println!("Sent invites to {} user(s).", num_users); //
} // Safely get first email from list of emails in field
Err(e) => { if let Some(user_email) = ldap_user
println!("Error: Failed to get existing users from Bitwarden"); .attrs
return Err(e); .get(mail_field.as_str())
.and_then(|l| (l.first()))
{
if existing_users.contains(&user_email.to_lowercase()) {
println!("User with email already exists: {}", user_email);
} else {
println!("Try to invite user: {}", user_email);
client
.invite(user_email)
.context(format!("Failed to invite user {}", user_email))?;
num_users += 1;
}
} else {
match ldap_user.attrs.get("uid").and_then(|l| l.first()) {
Some(user_uid) => println!(
"Warning: Email field, {:?}, not found on user {}",
mail_field, user_uid
),
None => println!("Warning: Email field, {:?}, not found on user", mail_field),
}
} }
} }
// Maybe think about returning this value for some other use
println!("Sent invites to {} user(s).", num_users);
Ok(()) Ok(())
} }
/// Begin sync loop to invite LDAP users to Bitwarden /// Begin sync loop to invite LDAP users to Bitwarden
fn start_sync_loop( fn start_sync_loop(config: &config::Config, client: &mut vw_admin::Client) -> Result<(), AnyError> {
config: &config::Config,
client: &mut vw_admin::Client,
) -> Result<(), Box<dyn Error>> {
let interval = Duration::from_secs(config.get_ldap_sync_interval_seconds()); let interval = Duration::from_secs(config.get_ldap_sync_interval_seconds());
let mut fail_count = 0;
let fail_limit = 5;
loop { loop {
invite_from_ldap(config, client)?; if let Err(err) = invite_from_ldap(config, client) {
println!(
"Error inviting users from ldap. Count {}: {:?}",
fail_count, err
);
fail_count += 1;
if fail_count > fail_limit {
return Err(err);
}
} else {
fail_count = 0
}
sleep(interval); sleep(interval);
} }
} }

View File

@ -1,16 +1,26 @@
extern crate reqwest; extern crate reqwest;
extern crate serde; extern crate serde;
extern crate thiserror;
use reqwest::Response; use reqwest::Response;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::error::Error;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use thiserror::Error;
const COOKIE_LIFESPAN: Duration = Duration::from_secs(20 * 60); const COOKIE_LIFESPAN: Duration = Duration::from_secs(20 * 60);
#[derive(Error, Debug)]
pub enum ResponseError {
#[error("vaultwarden error {0}")]
ApiError(String),
#[error("http error making request {0:?}")]
HttpError(#[from] reqwest::Error),
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct User { pub struct User {
#[serde(rename = "Email")] #[serde(rename = "Email")]
@ -59,7 +69,7 @@ impl Client {
.read_to_end(&mut buf) .read_to_end(&mut buf)
.expect("Could not read root cert file"); .expect("Could not read root cert file");
return reqwest::Certificate::from_der(&buf).expect("Could not load der root cert file"); reqwest::Certificate::from_der(&buf).expect("Could not load DER root cert file")
} }
fn get_http_client(&self) -> reqwest::Client { fn get_http_client(&self) -> reqwest::Client {
@ -70,104 +80,97 @@ impl Client {
client = client.add_root_certificate(cert); client = client.add_root_certificate(cert);
} }
return client.build().unwrap(); client.build().expect("Failed to build http client")
} }
/// Authenticate client /// Authenticate client
fn auth(&mut self) -> Response { fn auth(&mut self) -> Result<Response, ResponseError> {
let cookie_created = Instant::now(); let cookie_created = Instant::now();
let client = self.get_http_client(); let client = self.get_http_client();
let result = client let result = client
.post(format!("{}{}", &self.url, "/admin/").as_str()) .post(format!("{}{}", &self.url, "/admin/").as_str())
.form(&[("token", &self.admin_token)]) .form(&[("token", &self.admin_token)])
.send() .send()?
.unwrap_or_else(|e| { .error_for_status()?;
panic!("Could not authenticate with {}. {:?}", &self.url, e);
});
// TODO: Handle error statuses let cookie = result
.headers()
.get(reqwest::header::SET_COOKIE)
.ok_or_else(|| {
ResponseError::ApiError(String::from("Could not read authentication cookie"))
})?;
if let Some(cookie) = result.headers().get(reqwest::header::SET_COOKIE) { self.cookie = cookie.to_str().map(String::from).ok();
self.cookie = cookie.to_str().map(|s| String::from(s)).ok(); self.cookie_created = Some(cookie_created);
self.cookie_created = Some(cookie_created);
} else { Ok(result)
panic!("Could not authenticate.") }
fn cookie_expired(&self) -> bool {
match &self.cookie {
Some(_) => self
.cookie_created
.map_or(true, |created| (created.elapsed() >= COOKIE_LIFESPAN)),
None => true,
} }
result
} }
/// Ensure that the client has a current auth cookie /// Ensure that the client has a current auth cookie
fn ensure_auth(&mut self) { fn ensure_auth(&mut self) -> Result<(), ResponseError> {
match &self.cookie { if self.cookie_expired() {
Some(_) => { match self.auth() {
if self Ok(_) => Ok(()),
.cookie_created Err(err) => Err(err),
.map_or(true, |created| (created.elapsed() >= COOKIE_LIFESPAN)) }?
{ }
self.auth();
} Ok(())
}
None => {
self.auth();
}
};
// TODO: handle errors
} }
/// Make an authenticated GET to Bitwarden Admin /// Make an authenticated GET to Bitwarden Admin
fn get(&mut self, path: &str) -> Response { fn get(&mut self, path: &str) -> Result<Response, ResponseError> {
self.ensure_auth(); self.ensure_auth()?;
match &self.cookie { let url = format!("{}/admin{}", &self.url, path);
None => { let client = self.get_http_client();
panic!("We haven't authenticated. Must be an error"); let request = client.get(url.as_str()).header(
} reqwest::header::COOKIE,
Some(cookie) => { self.cookie
let url = format!("{}/admin{}", &self.url, path); .as_ref()
let client = self.get_http_client(); .expect("No cookie found to add to header")
let request = client .clone(),
.get(url.as_str()) );
.header(reqwest::header::COOKIE, cookie.clone());
let response = request.send().unwrap_or_else(|e| {
panic!("Could not call with {}. {:?}", url, e);
});
// TODO: Handle error statuses let response = request.send()?.error_for_status()?;
return response; Ok(response)
}
}
} }
/// Make authenticated POST to Bitwarden Admin with JSON data /// Make authenticated POST to Bitwarden Admin with JSON data
fn post(&mut self, path: &str, json: &HashMap<String, String>) -> Response { fn post(
self.ensure_auth(); &mut self,
path: &str,
json: &HashMap<String, String>,
) -> Result<Response, ResponseError> {
self.ensure_auth()?;
match &self.cookie { let url = format!("{}/admin{}", &self.url, path);
None => { let client = self.get_http_client();
panic!("We haven't authenticated. Must be an error"); let request = client.post(url.as_str()).json(&json).header(
} reqwest::header::COOKIE,
Some(cookie) => { self.cookie
let url = format!("{}/admin{}", &self.url, path); .as_ref()
let client = self.get_http_client(); .expect("No cookie found to add to header")
let request = client .clone(),
.post(url.as_str()) );
.header("Cookie", cookie.clone())
.json(&json);
let response = request.send().unwrap_or_else(|e| {
panic!("Could not call with {}. {:?}", url, e);
});
// TODO: Handle error statuses let response = request.send()?.error_for_status()?;
return response; Ok(response)
}
}
} }
/// Invite user with provided email /// Invite user with provided email
pub fn invite(&mut self, email: &str) -> Response { pub fn invite(&mut self, email: &str) -> Result<Response, ResponseError> {
let mut json = HashMap::new(); let mut json = HashMap::new();
json.insert("email".to_string(), email.to_string()); json.insert("email".to_string(), email.to_string());
@ -175,8 +178,8 @@ impl Client {
} }
/// Get all existing users /// Get all existing users
pub fn users(&mut self) -> Result<Vec<User>, Box<dyn Error>> { pub fn users(&mut self) -> Result<Vec<User>, ResponseError> {
let all_users: Vec<User> = self.get("/users").json()?; let all_users: Vec<User> = self.get("/users")?.json()?;
Ok(all_users) Ok(all_users)
} }
} }