diff --git a/README.md b/README.md index 70dfe37..7941efd 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,11 @@ Configuration values are as follows: |`ldap_search_filter`|String||Filter used when searching LDAP for users. Eg. `(&(objectClass=*)(uid=*))`| |`ldap_mail_field`|String|Optional|Field for each user record that contains the email address to use. Defaults to `mail`| |`ldap_sync_interval_seconds`|Integer|Optional|Number of seconds to wait between each LDAP request. Defaults to `60`| +|`ldap_sync_loop`|Boolean|Optional|Indicates whether or not syncing should be polled in a loop or done once. Defaults to `true`| ## Future * Query existing users to avoid redundant invites * Command line flags to select if polling is desired * Any kind of proper logging -* Tests \ No newline at end of file +* Tests diff --git a/src/bw_admin.rs b/src/bw_admin.rs index 7c5de74..1f3995b 100644 --- a/src/bw_admin.rs +++ b/src/bw_admin.rs @@ -9,19 +9,26 @@ use std::time::{Duration, Instant}; const COOKIE_LIFESPAN: Duration = Duration::from_secs(20 * 60); +fn true_val() -> bool { + true +} + +#[derive(Debug)] #[derive(Deserialize)] pub struct User { - Email: String, + #[serde(rename = "Email")] + email: String, #[serde(rename = "_Enabled")] - Enabled: bool, + #[serde(default = "true_val")] + enabled: bool, } impl User { pub fn get_email(&self) -> String { - self.Email.clone() + self.email.clone() } pub fn is_enabled(&self) -> bool { - self.Enabled + self.enabled } } @@ -151,4 +158,18 @@ impl Client { let all_users: Vec = self.get("/users").json()?; Ok(all_users) } + + /// Get all invited users + pub fn invites(&mut self) -> Result, Box> { + let all_invites: Vec = self.get("/invites").json()?; + Ok(all_invites) + } + + /// Get all users and invites + pub fn users_and_invites(&mut self) -> Result, Box> { + let mut all_users = self.users()?; + let mut invites = self.invites()?; + all_users.append(&mut invites); + Ok(all_users) + } } diff --git a/src/config.rs b/src/config.rs index 5b3c12c..c08324b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -53,6 +53,8 @@ pub struct Config { ldap_mail_field: Option, // Interval syncing config ldap_sync_interval_seconds: Option, + // Should start background sync loop + ldap_sync_loop: Option, } impl Config { @@ -139,4 +141,8 @@ impl Config { pub fn get_ldap_sync_interval_seconds(&self) -> u64 { self.ldap_sync_interval_seconds.unwrap_or(60) } + + pub fn get_ldap_sync_loop(&self) -> bool { + self.ldap_sync_loop.unwrap_or(true) + } } diff --git a/src/main.rs b/src/main.rs index dc6e3d8..560bb37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,6 @@ extern crate ldap3; use std::collections::HashSet; use std::error::Error; -use std::env; use std::thread::sleep; use std::time::Duration; @@ -11,24 +10,6 @@ use ldap3::{DerefAliases, LdapConn, Scope, SearchEntry, SearchOptions}; mod bw_admin; mod config; -/// Container for args parsed from the command line -struct ParsedArgs { - start_loop: bool, -} - -impl ParsedArgs { - pub parse() -> ParsedArgs { - let mut parsed_args = ParsedArgs {}; - for arg in env::args().collect() { - if arg == "--loop" { - parsed_args.start_loop = true; - } - } - - parsed_args.clone() - } -} - fn main() { let config = config::Config::from_file(); let mut client = bw_admin::Client::new( @@ -36,8 +17,7 @@ fn main() { config.get_bitwarden_admin_token().clone(), ); - let parsed_args = ParsedArgs::parse(); - if let Err(e) = invite_users(&config, &mut client, parsed_args.start_loop) { + if let Err(e) = invite_users(&config, &mut client, config.get_ldap_sync_loop()) { panic!("{}", e); } } @@ -47,13 +27,13 @@ fn invite_users( config: &config::Config, client: &mut bw_admin::Client, start_loop: bool, -) -> Result((), Box> { - let user_emails = get_existing_users(&mut client)?; +) -> Result<(), Box> { + // TODO: Better error handling to differentiate failure to connect to Bitwarden vs LDAP if start_loop { - start_sync_loop(&config, &mut client)?; + start_sync_loop(config, client)?; } else { - invite_from_ldap(&config, &mut client)?; + invite_from_ldap(config, client)?; } Ok(()) @@ -61,10 +41,15 @@ fn invite_users( /// Creates set of email addresses for users that already exist in Bitwarden fn get_existing_users(client: &mut bw_admin::Client) -> Result, Box> { - let all_users = client.users()?; + let all_users = client.users_and_invites()?; let mut user_emails = HashSet::with_capacity(all_users.len()); - for user in client.users()? { + for user in all_users { user_emails.insert(user.get_email()); + if user.is_enabled() { + println!("Existing user or invite found with email: {}", user.get_email()); + } else { + println!("Existing disabled user found with email: {}", user.get_email()); + } } Ok(user_emails) @@ -88,6 +73,10 @@ fn search_entries(config: &config::Config) -> Result, Box Result<(), Box> { + let existing_users = get_existing_users(client)?; + let mail_field = config.get_ldap_mail_field(); + let mut num_users = 0; for ldap_user in search_entries(config)? { if let Some(user_email) = ldap_user.attrs[mail_field.as_str()].first() { - println!("Try to invite user: {}", user_email); - let response = client.invite(user_email); - println!("Invite response: {:?}", response); + if existing_users.contains(user_email) { + println!("User with email already exists: {}", user_email); + } else { + println!("Try to invite user: {}", user_email); + let response = client.invite(user_email); + num_users = num_users + 1; + println!("Invite response: {:?}", response); + } } } + // Maybe think about returning this value for some other use + println!("Sent invites to {} user(s).", num_users); + Ok(()) }