From 6d79984a65325756793773b29626435f4d44978c Mon Sep 17 00:00:00 2001 From: ViViDboarder Date: Fri, 29 Mar 2019 11:18:56 -0700 Subject: [PATCH] LDAP querying complete --- .gitignore | 1 + Cargo.toml | 10 +++ README.md | 2 +- docker-compose-ldap-server.yml | 29 ++++++++ src/config.rs | 126 +++++++++++++++++++++++++++++++++ src/main.rs | 100 ++++++++++++++++++++++++++ 6 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 Cargo.toml create mode 100644 docker-compose-ldap-server.yml create mode 100644 src/config.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore index 088ba6b..72e4d68 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk +config.toml diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7131a89 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "bitwarden_rs_ldap" +version = "0.1.0" +authors = ["ViViDboarder "] +edition = "2018" + +[dependencies] +ldap3 = "0.6" +serde = { version = "1.0", features = ["derive"] } +toml = "0.5" diff --git a/README.md b/README.md index d530dbb..e8f7451 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# bwrs_ldap_directory +# bitwarden_rs_ldap LDAP directory connector for bitwarden_rs diff --git a/docker-compose-ldap-server.yml b/docker-compose-ldap-server.yml new file mode 100644 index 0000000..bc62bc6 --- /dev/null +++ b/docker-compose-ldap-server.yml @@ -0,0 +1,29 @@ +version: '3' +services: + ldap: + image: osixia/openldap + ports: + - 389:389 + - 636:636 + volumes: + - /var/lib/ldap + - /etc/ldap/slapd.d + environment: + LDAP_READONLY_USER: 'true' + LDAP_READONLY_USER_USERNAME: readonly + LDAP_READONLY_USER_PASSWORD: readonly + admin: + image: osixia/phpldapadmin + ports: + - 8001:80 + environment: + PHPLDAPADMIN_HTTPS: 'false' + PHPLDAPADMIN_LDAP_HOSTS: ldap + admin-host: + image: osixia/phpldapadmin + ports: + - 80:80 + network_mode: "host" + environment: + PHPLDAPADMIN_HTTPS: 'false' + PHPLDAPADMIN_LDAP_HOSTS: 0.0.0.0 diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..5be484b --- /dev/null +++ b/src/config.rs @@ -0,0 +1,126 @@ +extern crate serde; + +use std::env; +use std::fs; + +use serde::Deserialize; + +pub type Pass = String; + +const CONFIG_PATH_DEFAULT: &str = "config.toml"; + +/// Returns config path from envioronment or a provided default value +pub fn get_config_path() -> String { + match env::var("CONFIG_PATH") { + Ok(config_path) => config_path, + Err(_) => String::from(CONFIG_PATH_DEFAULT), + } +} + +/// Reads configuration from file and panics if it can't +pub fn read_config() -> Config { + let config_path = get_config_path(); + + let contents = fs::read_to_string(&config_path).unwrap_or_else(|_| { + panic!("Failed to open config file at {}", config_path); + }); + let config: Config = toml::from_str(contents.as_str()).unwrap_or_else(|_| { + panic!("Failed to parse config file at {}", config_path); + }); + + config +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +/// Contains all config values for LDAP syncing +pub struct Config { + ldap_host: String, + ldap_scheme: Option, + ldap_ssl: Option, + ldap_port: Option, + ldap_bind_dn: String, + ldap_bind_password: Pass, + ldap_search_base_dn: String, + ldap_search_filter: String, + ldap_mail_field: Option, + ldap_sync_interval_seconds: Option, +} + +impl Config { + /// Create a config instance from file + pub fn from_file() -> Config { + read_config() + } + + pub fn get_ldap_url(&self) -> String { + format!( + "{}://{}:{}", + self.get_ldap_scheme(), + self.get_ldap_host(), + self.get_ldap_port() + ) + } + + pub fn get_ldap_host(&self) -> String { + self.ldap_host.clone() + } + + pub fn get_ldap_scheme(&self) -> String { + match &self.ldap_scheme { + Some(ldap_scheme) => ldap_scheme.clone(), + None => { + if self.get_ldap_ssl() { + String::from("ldaps") + } else { + String::from("ldap") + } + } + } + } + + pub fn get_ldap_ssl(&self) -> bool { + self.ldap_ssl.unwrap_or(false) + } + + pub fn get_ldap_port(&self) -> u16 { + match self.ldap_port { + Some(ldap_port) => ldap_port, + None => { + if self.get_ldap_ssl() { + 636 + } else { + 389 + } + } + } + } + + pub fn get_ldap_bind_dn(&self) -> String { + self.ldap_bind_dn.clone() + } + + pub fn get_ldap_bind_password(&self) -> String { + self.ldap_bind_password.clone() + } + + pub fn get_ldap_search_base_dn(&self) -> String { + self.ldap_search_base_dn.clone() + } + + pub fn get_ldap_search_filter(&self) -> String { + self.ldap_search_filter.clone() + } + + pub fn get_ldap_mail_field(&self) -> String { + let default = String::from("mail"); + match &self.ldap_mail_field { + Some(mail_field) => mail_field.clone(), + None => default.clone(), + } + } + + pub fn get_ldap_sync_interval_seconds(&self) -> u64 { + self.ldap_sync_interval_seconds.unwrap_or(60) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6696716 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,100 @@ +extern crate ldap3; + +use std::error::Error; +use std::thread::sleep; +use std::time::Duration; + +use ldap3::{DerefAliases, LdapConn, Scope, SearchEntry, SearchOptions}; + +mod config; + +fn main() { + let config = config::Config::from_file(); + + match do_search(&config) { + Ok(_) => (), + Err(e) => println!("{}", e), + } + + if let Err(e) = invite_from_ldap(&config) { + println!("{}", e); + } + + if let Err(e) = start_sync_loop(&config) { + println!("{}", e); + } +} + +/// Creates an LDAP connection, authenticating if necessary +fn ldap_client(ldap_url: String, bind_dn: String, bind_pw: String) -> Result> { + let ldap = LdapConn::new(ldap_url.as_str())?; + match ldap.simple_bind(bind_dn.as_str(), bind_pw.as_str()) { + _ => {} + }; + + Ok(ldap) +} + +/// Retrieves search results from ldap +fn search_entries(config: &config::Config) -> Result, Box> { + let ldap = ldap_client( + config.get_ldap_url(), + config.get_ldap_bind_dn(), + config.get_ldap_bind_password(), + ); + + let mail_field = config.get_ldap_mail_field(); + let fields = vec!["uid", "givenname", "sn", "cn", mail_field.as_str()]; + + // TODO: 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, + )? + .success()?; + + // Build list of entries + let mut entries = Vec::new(); + for result in results { + entries.push(SearchEntry::construct(result)); + } + + Ok(entries) +} + +/// Perform a simple search and list users +fn do_search(config: &config::Config) -> Result<(), Box> { + let mail_field = config.get_ldap_mail_field(); + let entries = search_entries(config)?; + for user in entries { + println!("{:?}", user); + if let Some(user_email) = user.attrs[mail_field.as_str()].first() { + println!("{}", user_email); + } + } + + Ok(()) +} + +fn invite_from_ldap(config: &config::Config) -> Result<(), Box> { + let mail_field = config.get_ldap_mail_field(); + 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); + } + } + + Ok(()) +} + +fn start_sync_loop(config: &config::Config) -> Result<(), Box> { + let interval = Duration::from_secs(config.get_ldap_sync_interval_seconds()); + loop { + invite_from_ldap(config)?; + sleep(interval); + } +}