mirror of
https://github.com/ViViDboarder/bitwarden_rs_ldap.git
synced 2024-11-22 11:16:27 +00:00
Improved error handling
Massive refactor of error handling to make messages easier to debug
This commit is contained in:
parent
c090fb5a52
commit
77919aa3d4
1465
Cargo.lock
generated
1465
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
|
@ -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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
149
src/main.rs
149
src/main.rs
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
149
src/vw_admin.rs
149
src/vw_admin.rs
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user