extern crate anyhow; extern crate ldap3; extern crate pledge; extern crate unveil; use std::collections::HashSet; use std::thread::sleep; 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 pledge::pledge; use unveil::unveil; mod config; mod vw_admin; fn main() { let config = config::Config::from_file(); let mut client = vw_admin::Client::new( config.get_vaultwarden_url(), config.get_vaultwarden_admin_token(), config.get_vaultwarden_root_cert_file(), ); unveil(config::get_config_path(), "r") .or_else(unveil::Error::ignore_platform) .expect("Could not unveil config file"); unveil("", "") .or_else(unveil::Error::ignore_platform) .expect("Could not disable further unveils"); pledge("dns flock inet rpath stdio tty", "") .or_else(pledge::Error::ignore_platform) .expect("Could not pledge permissions"); invite_users(&config, &mut client, config.get_ldap_sync_loop()) } /// Invites new users to Bitwarden from LDAP fn invite_users(config: &config::Config, client: &mut vw_admin::Client, start_loop: bool) { if start_loop { start_sync_loop(config, client).expect("Failed to start invite sync loop"); } else { invite_from_ldap(config, client).expect("Failed to invite users"); } } /// Creates set of email addresses for users that already exist in Bitwarden fn get_existing_users(client: &mut vw_admin::Client) -> Result, AnyError> { 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()); for user in all_users { user_emails.insert(user.get_email().to_lowercase()); if user.is_disabled() { println!( "Existing disabled user found with email: {}", user.get_email() ); } else { println!( "Existing user or invite found with email: {}", user.get_email() ); } } Ok(user_emails) } /// Creates an LDAP connection, authenticating if necessary fn ldap_client( ldap_url: String, bind_dn: String, bind_pw: String, no_tls_verify: bool, starttls: bool, ) -> Result { let settings = LdapConnSettings::new() .set_starttls(starttls) .set_no_tls_verify(no_tls_verify); let mut ldap = LdapConn::with_settings(settings, ldap_url.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) } /// Retrieves search results from ldap fn search_entries(config: &config::Config) -> Result, AnyError> { let mut ldap = ldap_client( config.get_ldap_url(), config.get_ldap_bind_dn(), config.get_ldap_bind_password(), config.get_ldap_no_tls_verify(), config.get_ldap_starttls(), ) .context("LDAP client initialization failed")?; let mail_field = config.get_ldap_mail_field(); let fields = vec!["uid", "givenname", "sn", "cn", mail_field.as_str()]; // Something something error handling let (results, _res) = ldap .with_search_options(SearchOptions::new().deref(DerefAliases::Always)) .search( config.get_ldap_search_base_dn().as_str(), Scope::Subtree, config.get_ldap_search_filter().as_str(), fields, ) .context("LDAP search failure")? .success() .context("LDAP search usucessful")?; // Build list of entries let mut entries = Vec::new(); for result in results { entries.push(SearchEntry::construct(result)); } Ok(entries) } /// Invite all LDAP users to Bitwarden fn invite_from_ldap( config: &config::Config, client: &mut vw_admin::Client, ) -> Result<(), AnyError> { let existing_users = get_existing_users(client).context("Failed to get existing users from server")?; let mail_field = config.get_ldap_mail_field(); 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); 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(()) } /// Begin sync loop to invite LDAP users to Bitwarden fn start_sync_loop(config: &config::Config, client: &mut vw_admin::Client) -> Result<(), AnyError> { let interval = Duration::from_secs(config.get_ldap_sync_interval_seconds()); let mut fail_count = 0; let fail_limit = 5; loop { 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); } }