Add checking of both users and invites before sending new ones

This commit is contained in:
ViViDboarder 2019-04-11 16:20:00 -07:00
parent 57851489cb
commit d1b467a148
4 changed files with 63 additions and 35 deletions

View File

@ -23,10 +23,11 @@ Configuration values are as follows:
|`ldap_search_filter`|String||Filter used when searching LDAP for users. Eg. `(&(objectClass=*)(uid=*))`| |`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_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_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 ## Future
* Query existing users to avoid redundant invites * Query existing users to avoid redundant invites
* Command line flags to select if polling is desired * Command line flags to select if polling is desired
* Any kind of proper logging * Any kind of proper logging
* Tests * Tests

View File

@ -9,19 +9,26 @@ use std::time::{Duration, Instant};
const COOKIE_LIFESPAN: Duration = Duration::from_secs(20 * 60); const COOKIE_LIFESPAN: Duration = Duration::from_secs(20 * 60);
fn true_val() -> bool {
true
}
#[derive(Debug)]
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct User { pub struct User {
Email: String, #[serde(rename = "Email")]
email: String,
#[serde(rename = "_Enabled")] #[serde(rename = "_Enabled")]
Enabled: bool, #[serde(default = "true_val")]
enabled: bool,
} }
impl User { impl User {
pub fn get_email(&self) -> String { pub fn get_email(&self) -> String {
self.Email.clone() self.email.clone()
} }
pub fn is_enabled(&self) -> bool { pub fn is_enabled(&self) -> bool {
self.Enabled self.enabled
} }
} }
@ -151,4 +158,18 @@ impl Client {
let all_users: Vec<User> = self.get("/users").json()?; let all_users: Vec<User> = self.get("/users").json()?;
Ok(all_users) Ok(all_users)
} }
/// Get all invited users
pub fn invites(&mut self) -> Result<Vec<User>, Box<Error>> {
let all_invites: Vec<User> = self.get("/invites").json()?;
Ok(all_invites)
}
/// Get all users and invites
pub fn users_and_invites(&mut self) -> Result<Vec<User>, Box<Error>> {
let mut all_users = self.users()?;
let mut invites = self.invites()?;
all_users.append(&mut invites);
Ok(all_users)
}
} }

View File

@ -53,6 +53,8 @@ pub struct Config {
ldap_mail_field: Option<String>, ldap_mail_field: Option<String>,
// Interval syncing config // Interval syncing config
ldap_sync_interval_seconds: Option<u64>, ldap_sync_interval_seconds: Option<u64>,
// Should start background sync loop
ldap_sync_loop: Option<bool>,
} }
impl Config { impl Config {
@ -139,4 +141,8 @@ impl Config {
pub fn get_ldap_sync_interval_seconds(&self) -> u64 { pub fn get_ldap_sync_interval_seconds(&self) -> u64 {
self.ldap_sync_interval_seconds.unwrap_or(60) self.ldap_sync_interval_seconds.unwrap_or(60)
} }
pub fn get_ldap_sync_loop(&self) -> bool {
self.ldap_sync_loop.unwrap_or(true)
}
} }

View File

@ -2,7 +2,6 @@ extern crate ldap3;
use std::collections::HashSet; use std::collections::HashSet;
use std::error::Error; use std::error::Error;
use std::env;
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
@ -11,24 +10,6 @@ use ldap3::{DerefAliases, LdapConn, Scope, SearchEntry, SearchOptions};
mod bw_admin; mod bw_admin;
mod config; 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() { fn main() {
let config = config::Config::from_file(); let config = config::Config::from_file();
let mut client = bw_admin::Client::new( let mut client = bw_admin::Client::new(
@ -36,8 +17,7 @@ fn main() {
config.get_bitwarden_admin_token().clone(), config.get_bitwarden_admin_token().clone(),
); );
let parsed_args = ParsedArgs::parse(); if let Err(e) = invite_users(&config, &mut client, config.get_ldap_sync_loop()) {
if let Err(e) = invite_users(&config, &mut client, parsed_args.start_loop) {
panic!("{}", e); panic!("{}", e);
} }
} }
@ -47,13 +27,13 @@ fn invite_users(
config: &config::Config, config: &config::Config,
client: &mut bw_admin::Client, client: &mut bw_admin::Client,
start_loop: bool, start_loop: bool,
) -> Result((), Box<Error>> { ) -> Result<(), Box<Error>> {
let user_emails = get_existing_users(&mut client)?; // TODO: Better error handling to differentiate failure to connect to Bitwarden vs LDAP
if start_loop { if start_loop {
start_sync_loop(&config, &mut client)?; start_sync_loop(config, client)?;
} else { } else {
invite_from_ldap(&config, &mut client)?; invite_from_ldap(config, client)?;
} }
Ok(()) Ok(())
@ -61,10 +41,15 @@ fn invite_users(
/// 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 bw_admin::Client) -> Result<HashSet<String>, Box<Error>> { fn get_existing_users(client: &mut bw_admin::Client) -> Result<HashSet<String>, Box<Error>> {
let all_users = client.users()?; let all_users = client.users_and_invites()?;
let mut user_emails = HashSet::with_capacity(all_users.len()); 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()); 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) Ok(user_emails)
@ -88,6 +73,10 @@ fn search_entries(config: &config::Config) -> Result<Vec<SearchEntry>, Box<Error
config.get_ldap_bind_password(), config.get_ldap_bind_password(),
); );
if ldap.is_err() {
println!("Error: Could not connect 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()];
@ -116,15 +105,26 @@ fn invite_from_ldap(
config: &config::Config, config: &config::Config,
client: &mut bw_admin::Client, client: &mut bw_admin::Client,
) -> Result<(), Box<Error>> { ) -> Result<(), Box<Error>> {
let existing_users = get_existing_users(client)?;
let mail_field = config.get_ldap_mail_field(); let mail_field = config.get_ldap_mail_field();
let mut num_users = 0;
for ldap_user in search_entries(config)? { for ldap_user in search_entries(config)? {
if let Some(user_email) = ldap_user.attrs[mail_field.as_str()].first() { if let Some(user_email) = ldap_user.attrs[mail_field.as_str()].first() {
println!("Try to invite user: {}", user_email); if existing_users.contains(user_email) {
let response = client.invite(user_email); println!("User with email already exists: {}", user_email);
println!("Invite response: {:?}", response); } 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(()) Ok(())
} }