diff --git a/Cargo.lock b/Cargo.lock index 27ce62d..45eb982 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2323,7 +2323,7 @@ dependencies = [ "peg", "tokio-util", "tracing", - "uuid 1.3.0", + "uuid 1.3.1", ] [[package]] @@ -2463,7 +2463,7 @@ dependencies = [ "tracing-forest", "tracing-log", "tracing-subscriber", - "uuid 1.3.0", + "uuid 0.8.2", "webpki-roots", ] @@ -2589,6 +2589,12 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.5.0" @@ -3557,7 +3563,7 @@ dependencies = [ "thiserror", "tracing", "url", - "uuid 1.3.0", + "uuid 1.3.1", ] [[package]] @@ -3581,7 +3587,7 @@ checksum = "d2fbe015dbdaa7d8829d71c1e14fb6289e928ac256b93dfda543c85cd89d6f03" dependencies = [ "chrono", "sea-query-derive", - "uuid 1.3.0", + "uuid 1.3.1", ] [[package]] @@ -3593,7 +3599,7 @@ dependencies = [ "chrono", "sea-query", "sqlx", - "uuid 1.3.0", + "uuid 1.3.1", ] [[package]] @@ -3977,7 +3983,7 @@ dependencies = [ "thiserror", "tokio-stream", "url", - "uuid 1.3.0", + "uuid 1.3.1", "webpki-roots", "whoami", ] @@ -4300,7 +4306,7 @@ dependencies = [ "actix-web", "pin-project", "tracing", - "uuid 1.3.0", + "uuid 1.3.1", ] [[package]] @@ -4498,15 +4504,18 @@ name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.8", + "md5", +] [[package]] name = "uuid" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb" dependencies = [ "getrandom 0.2.8", - "md-5", ] [[package]] diff --git a/server/Cargo.toml b/server/Cargo.toml index 6208bc5..8f16bf2 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -142,4 +142,8 @@ version = "0.11" [dev-dependencies.reqwest] version = "*" default-features = false -features = ["json", "blocking", "rustls-tls"] \ No newline at end of file +features = ["json", "blocking", "rustls-tls"] + +[dev-dependencies.uuid] +version = "*" +features = ["v4"] \ No newline at end of file diff --git a/server/tests/common/auth.rs b/server/tests/common/auth.rs new file mode 100644 index 0000000..4e966f4 --- /dev/null +++ b/server/tests/common/auth.rs @@ -0,0 +1,27 @@ +use crate::common::env; +use reqwest::blocking::Client; + +pub fn get_token(client: &Client) -> String { + let username = env::admin_dn(); + let password = env::admin_password(); + let base_url = env::http_url(); + let response = client + .post(format!("{base_url}/auth/simple/login")) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .body( + serde_json::to_string(&lldap_auth::login::ClientSimpleLoginRequest { + username: username, + password: password, + }) + .expect("Failed to encode the username/password as json to log in"), + ) + .send() + .expect("Failed to send auth request") + .error_for_status() + .expect("Auth attempt failed"); + serde_json::from_str::( + &response.text().expect("Failed to get response as text"), + ) + .expect("Failed to parse json") + .token +} diff --git a/server/tests/common/env.rs b/server/tests/common/env.rs new file mode 100644 index 0000000..7d17ced --- /dev/null +++ b/server/tests/common/env.rs @@ -0,0 +1,43 @@ +use std::env::var; + +pub const DB_KEY: &str = "LLDAP_DATABASE_URL"; + +pub fn database_url() -> String { + let url = var(DB_KEY).ok(); + let url = url.unwrap_or("sqlite://e2e_test.db?mode=rwc".to_string()); + url.to_string() +} + +pub fn ldap_url() -> String { + let port = var("LLDAP_LDAP_PORT").ok(); + let port = port.unwrap_or("3890".to_string()); + let mut url = String::from("ldap://localhost:"); + url += &port; + url +} + +pub fn http_url() -> String { + let port = var("LLDAP_HTTP_PORT").ok(); + let port = port.unwrap_or("17170".to_string()); + let mut url = String::from("http://localhost:"); + url += &port; + url +} + +pub fn admin_dn() -> String { + let user = var("LLDAP_LDAP_USER_DN").ok(); + let user = user.unwrap_or("admin".to_string()); + user.to_string() +} + +pub fn admin_password() -> String { + let pass = var("LLDAP_LDAP_USER_PASS").ok(); + let pass = pass.unwrap_or("password".to_string()); + pass.to_string() +} + +pub fn base_dn() -> String { + let dn = var("LLDAP_LDAP_BASE_DN").ok(); + let dn = dn.unwrap_or("dc=example,dc=com".to_string()); + dn.to_string() +} diff --git a/server/tests/common/fixture.rs b/server/tests/common/fixture.rs index f2bde38..1e7de49 100644 --- a/server/tests/common/fixture.rs +++ b/server/tests/common/fixture.rs @@ -1,78 +1,12 @@ -use anyhow::{anyhow, bail, Context, Result}; +use crate::common::auth::get_token; +use crate::common::env; +use crate::common::graphql::*; use assert_cmd::prelude::*; -use graphql_client::GraphQLQuery; use reqwest::blocking::{Client, ClientBuilder}; use std::collections::{HashMap, HashSet}; use std::process::{Child, Command}; -use std::{env::var, fs::canonicalize, thread, time::Duration}; - -const DB_KEY: &str = "LLDAP_DATABASE_URL"; - -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "../schema.graphql", - query_path = "tests/queries/add_user_to_group.graphql", - response_derives = "Debug", - variables_derives = "Debug,Clone", - custom_scalars_module = "crate::infra::graphql" -)] -struct AddUserToGroup; - -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "../schema.graphql", - query_path = "tests/queries/create_user.graphql", - response_derives = "Debug", - variables_derives = "Debug,Clone", - custom_scalars_module = "crate::infra::graphql" -)] -struct CreateUser; - -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "../schema.graphql", - query_path = "tests/queries/create_group.graphql", - response_derives = "Debug", - variables_derives = "Debug,Clone", - custom_scalars_module = "crate::infra::graphql" -)] -struct CreateGroup; - -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "../schema.graphql", - query_path = "tests/queries/list_users.graphql", - response_derives = "Debug", - custom_scalars_module = "crate::infra::graphql" -)] -pub struct ListUsers; - -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "../schema.graphql", - query_path = "tests/queries/list_groups.graphql", - response_derives = "Debug", - custom_scalars_module = "crate::infra::graphql" -)] -struct ListGroups; - -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "../schema.graphql", - query_path = "tests/queries/delete_group.graphql", - response_derives = "Debug", - custom_scalars_module = "crate::infra::graphql" -)] -struct DeleteGroupQuery; - -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "../schema.graphql", - query_path = "tests/queries/delete_user.graphql", - response_derives = "Debug", - custom_scalars_module = "crate::infra::graphql" -)] -struct DeleteUserQuery; +use std::{fs::canonicalize, thread, time::Duration}; +use uuid::Uuid; #[derive(Clone)] pub struct User { @@ -101,11 +35,11 @@ impl LLDAPFixture { let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).expect("cargo bin found"); let path = canonicalize("..").expect("canonical path"); - let db_url = get_database_url(); + let db_url = env::database_url(); println!("Running from directory: {:?}", path); println!("Using database: {db_url}"); cmd.current_dir(path.clone()); - cmd.env(DB_KEY, db_url); + cmd.env(env::DB_KEY, db_url); cmd.arg("run"); cmd.arg("--verbose"); let child = cmd.spawn().expect("Unable to start server"); @@ -127,7 +61,7 @@ impl LLDAPFixture { .redirect(reqwest::redirect::Policy::none()) .build() .expect("failed to make http client"); - let token = get_token(&client).expect("failed to get token"); + let token = get_token(&client); Self { client, token, @@ -244,99 +178,11 @@ impl Drop for LLDAPFixture { } } -fn get_database_url() -> String { - let url = var(DB_KEY).ok(); - let url = url.unwrap_or("sqlite://e2e_test.db?mode=rwc".to_string()); - url.to_string() -} - -pub fn get_ldap_url() -> String { - let port = option_env!("LLDAP_LDAP_PORT"); - let port = port.unwrap_or("3890"); - let mut url = String::from("ldap://localhost:"); - url += port; - url -} - -pub fn get_http_url() -> String { - let port = option_env!("LLDAP_HTTP_PORT"); - let port = port.unwrap_or("17170"); - let mut url = String::from("http://localhost:"); - url += port; - url -} - -pub fn get_admin_dn() -> String { - let user = option_env!("LLDAP_LDAP_USER_DN"); - let user = user.unwrap_or("admin"); - user.to_string() -} - -pub fn get_admin_password() -> String { - let pass = option_env!("LLDAP_LDAP_USER_PASS"); - let pass = pass.unwrap_or("password"); - pass.to_string() -} - -pub fn get_base_dn() -> String { - let dn = option_env!("LLDAP_LDAP_BASE_DN"); - let dn = dn.unwrap_or("dc=example,dc=com"); - dn.to_string() -} - -pub fn get_token(client: &Client) -> Result { - let username = get_admin_dn(); - let password = get_admin_password(); - let base_url = get_http_url(); - let response = client - .post(format!("{base_url}/auth/simple/login")) - .header(reqwest::header::CONTENT_TYPE, "application/json") - .body( - serde_json::to_string(&lldap_auth::login::ClientSimpleLoginRequest { - username: username, - password: password, - }) - .expect("Failed to encode the username/password as json to log in"), - ) - .send()? - .error_for_status()?; - Ok(serde_json::from_str::(&response.text()?)?.token) -} -pub fn post( - client: &Client, - token: &String, - variables: QueryType::Variables, -) -> Result -where - QueryType: GraphQLQuery + 'static, -{ - let unwrap_graphql_response = |graphql_client::Response { data, errors, .. }| { - data.ok_or_else(|| { - anyhow!( - "Errors: [{}]", - errors - .unwrap_or_default() - .iter() - .map(ToString::to_string) - .collect::>() - .join(", ") - ) - }) - }; - let url = get_http_url() + "/api/graphql"; - let auth_header = format!("Bearer {}", token); - client - .post(url) - .header(reqwest::header::AUTHORIZATION, auth_header) - // Request body. - .json(&QueryType::build_query(variables)) - .send() - .context("while sending a request to the LLDAP server")? - .error_for_status() - .context("error from an LLDAP response")? - // Parse response as Json. - .json::>() - .context("while parsing backend response") - .and_then(unwrap_graphql_response) - .context("GraphQL error from an LLDAP response") +pub fn new_id(prefix: Option<&str>) -> String { + let id = Uuid::new_v4(); + let id = format!("{}-lldap-test", id.to_simple()); + match prefix { + Some(prefix) => format!("{}{}", prefix, id), + None => id, + } } diff --git a/server/tests/common/graphql.rs b/server/tests/common/graphql.rs new file mode 100644 index 0000000..cf31c92 --- /dev/null +++ b/server/tests/common/graphql.rs @@ -0,0 +1,109 @@ +use crate::common::env; +use anyhow::{anyhow, Context, Result}; +use graphql_client::GraphQLQuery; +use reqwest::blocking::Client; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "tests/queries/add_user_to_group.graphql", + response_derives = "Debug", + variables_derives = "Debug,Clone", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct AddUserToGroup; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "tests/queries/create_user.graphql", + response_derives = "Debug", + variables_derives = "Debug,Clone", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct CreateUser; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "tests/queries/create_group.graphql", + response_derives = "Debug", + variables_derives = "Debug,Clone", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct CreateGroup; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "tests/queries/list_users.graphql", + response_derives = "Debug", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct ListUsers; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "tests/queries/list_groups.graphql", + response_derives = "Debug", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct ListGroups; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "tests/queries/delete_group.graphql", + response_derives = "Debug", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct DeleteGroupQuery; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "tests/queries/delete_user.graphql", + response_derives = "Debug", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct DeleteUserQuery; + +pub fn post( + client: &Client, + token: &String, + variables: QueryType::Variables, +) -> Result +where + QueryType: GraphQLQuery + 'static, +{ + let unwrap_graphql_response = |graphql_client::Response { data, errors, .. }| { + data.ok_or_else(|| { + anyhow!( + "Errors: [{}]", + errors + .unwrap_or_default() + .iter() + .map(ToString::to_string) + .collect::>() + .join(", ") + ) + }) + }; + let url = env::http_url() + "/api/graphql"; + let auth_header = format!("Bearer {}", token); + client + .post(url) + .header(reqwest::header::AUTHORIZATION, auth_header) + // Request body. + .json(&QueryType::build_query(variables)) + .send() + .context("while sending a request to the LLDAP server")? + .error_for_status() + .context("error from an LLDAP response")? + // Parse response as Json. + .json::>() + .context("while parsing backend response") + .and_then(unwrap_graphql_response) + .context("GraphQL error from an LLDAP response") +} diff --git a/server/tests/common/mod.rs b/server/tests/common/mod.rs index 97b7845..7a54c77 100644 --- a/server/tests/common/mod.rs +++ b/server/tests/common/mod.rs @@ -1 +1,4 @@ -pub mod fixture; \ No newline at end of file +pub mod auth; +pub mod env; +pub mod fixture; +pub mod graphql; diff --git a/server/tests/graphql.rs b/server/tests/graphql.rs index 937f880..b9a181d 100644 --- a/server/tests/graphql.rs +++ b/server/tests/graphql.rs @@ -1,19 +1,27 @@ -use crate::common::fixture::{LLDAPFixture, User}; -use graphql_client::GraphQLQuery; -use ldap3::{LdapConn, Scope, SearchEntry}; -use reqwest::blocking::{Client, ClientBuilder}; +use crate::common::{ + auth::get_token, + fixture::{new_id, LLDAPFixture, User}, + graphql::{post, ListUsers}, +}; +use reqwest::blocking::ClientBuilder; use serial_test::file_serial; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; mod common; #[test] #[file_serial] fn list_users() { let mut fixture = LLDAPFixture::new(); + let prefix = "graphql-list_users-"; + let user1_name = new_id(Some(prefix)); + let user2_name = new_id(Some(prefix)); + let user3_name = new_id(Some(prefix)); + let group1_name = new_id(Some(prefix)); + let group2_name = new_id(Some(prefix)); let initial_state = vec![ - User::new("user1", vec!["group-one"]), - User::new("user2", vec!["group-one", "group-two"]), - User::new("user3", vec![]), + User::new(&user1_name, vec![&group1_name]), + User::new(&user2_name, vec![&group1_name, &group2_name]), + User::new(&user3_name, vec![]), ]; fixture.load_state(&initial_state); @@ -23,20 +31,11 @@ fn list_users() { .redirect(reqwest::redirect::Policy::none()) .build() .expect("failed to make http client"); - let token = common::fixture::get_token(&client).expect("failed to get token"); - let result = common::fixture::post::(&client, &token, list_users::Variables {}) + let token = get_token(&client); + let result = post::(&client, &token, common::graphql::list_users::Variables {}) .expect("failed to list users"); let users: HashSet = result.users.iter().map(|user| user.id.clone()).collect(); - assert!(users.contains("user1")); - assert!(users.contains("user2")); - assert!(users.contains("user3")); + assert!(users.contains(&user1_name)); + assert!(users.contains(&user2_name)); + assert!(users.contains(&user3_name)); } - -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "../schema.graphql", - query_path = "tests/queries/list_users.graphql", - response_derives = "Debug", - custom_scalars_module = "crate::infra::graphql" -)] -struct ListUsers; diff --git a/server/tests/integrations.rs b/server/tests/integrations.rs index 64cf264..88fc605 100644 --- a/server/tests/integrations.rs +++ b/server/tests/integrations.rs @@ -1,6 +1,9 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; -use crate::common::fixture::{LLDAPFixture, User}; +use crate::common::{ + env, + fixture::{new_id, LLDAPFixture, User}, +}; use ldap3::{LdapConn, Scope, SearchEntry}; use serial_test::file_serial; mod common; @@ -9,29 +12,26 @@ mod common; #[file_serial] fn gitea() { let mut fixture = LLDAPFixture::new(); - let gitea_user_group = "gitea_user"; + let gitea_user_group = new_id(Some("gitea_user-")); + let gitea_admin_group = new_id(Some("gitea_admin-")); + let gitea_user1 = new_id(Some("gitea1-")); + let gitea_user2 = new_id(Some("gitea2-")); + let gitea_user3 = new_id(Some("gitea3-")); let initial_state = vec![ - User::new("bob", vec![gitea_user_group, "gitea-admin"]), - User::new("alice", vec![gitea_user_group]), - User::new("james", vec![]), + User::new(&gitea_user1, vec![&gitea_user_group, &gitea_admin_group]), + User::new(&gitea_user2, vec![&gitea_user_group]), + User::new(&gitea_user3, vec![]), ]; fixture.load_state(&initial_state); - let mut ldap = LdapConn::new(common::fixture::get_ldap_url().as_str()) - .expect("failed to create ldap connection"); - let base_dn = common::fixture::get_base_dn(); - let bind_dn = format!( - "uid={},ou=people,{}", - common::fixture::get_admin_dn(), - base_dn - ); - ldap.simple_bind( - bind_dn.as_str(), - common::fixture::get_admin_password().as_str(), - ) - .expect("failed to bind to ldap"); + let mut ldap = + LdapConn::new(env::ldap_url().as_str()).expect("failed to create ldap connection"); + let base_dn = env::base_dn(); + let bind_dn = format!("uid={},ou=people,{}", env::admin_dn(), base_dn); + ldap.simple_bind(bind_dn.as_str(), env::admin_password().as_str()) + .expect("failed to bind to ldap"); - let user_base = format!("ou=people,{}", common::fixture::get_base_dn()); + let user_base = format!("ou=people,{}", base_dn); let attrs = vec!["uid", "givenName", "sn", "mail", "jpegPhoto"]; let results = ldap .search( @@ -50,8 +50,8 @@ fn gitea() { let user = attrs.get("uid").unwrap().get(0).unwrap(); found_users.insert(user.clone()); } - assert!(found_users.contains("bob")); - assert!(found_users.contains("alice")); - assert!(!found_users.contains("james")); + assert!(found_users.contains(&gitea_user1)); + assert!(found_users.contains(&gitea_user2)); + assert!(!found_users.contains(&gitea_user3)); ldap.unbind().expect("failed to unbind ldap connection"); } diff --git a/server/tests/ldap.rs b/server/tests/ldap.rs index 2c60d43..6ea52cf 100644 --- a/server/tests/ldap.rs +++ b/server/tests/ldap.rs @@ -1,6 +1,9 @@ use std::collections::{HashMap, HashSet}; -use crate::common::fixture::{LLDAPFixture, User}; +use crate::common::{ + env, + fixture::{new_id, LLDAPFixture, User}, +}; use ldap3::{LdapConn, Scope, SearchEntry}; use serial_test::file_serial; mod common; @@ -9,31 +12,30 @@ mod common; #[file_serial] fn basic_users_search() { let mut fixture = LLDAPFixture::new(); + let prefix = "ldap-basic_users_search-"; + let user1_name = new_id(Some(prefix)); + let user2_name = new_id(Some(prefix)); + let user3_name = new_id(Some(prefix)); + let group1_name = new_id(Some(prefix)); + let group2_name = new_id(Some(prefix)); let initial_state = vec![ - User::new("user1", vec!["group-one"]), - User::new("user2", vec!["group-one", "group-two"]), - User::new("user3", vec![]), + User::new(&user1_name, vec![&group1_name]), + User::new(&user2_name, vec![&group1_name, &group2_name]), + User::new(&user3_name, vec![]), ]; fixture.load_state(&initial_state); - let mut ldap = LdapConn::new(common::fixture::get_ldap_url().as_str()) - .expect("failed to create ldap connection"); - let base_dn = common::fixture::get_base_dn(); - let bind_dn = format!( - "uid={},ou=people,{}", - common::fixture::get_admin_dn(), - base_dn - ); - ldap.simple_bind( - bind_dn.as_str(), - common::fixture::get_admin_password().as_str(), - ) - .expect("failed to bind to ldap"); + let mut ldap = + LdapConn::new(env::ldap_url().as_str()).expect("failed to create ldap connection"); + let base_dn = env::base_dn(); + let bind_dn = format!("uid={},ou=people,{}", env::admin_dn(), base_dn); + ldap.simple_bind(bind_dn.as_str(), env::admin_password().as_str()) + .expect("failed to bind to ldap"); let attrs = vec!["uid", "memberof"]; let results = ldap .search( - common::fixture::get_base_dn().as_str(), + env::base_dn().as_str(), Scope::Subtree, "(objectclass=person)", attrs, @@ -51,21 +53,21 @@ fn basic_users_search() { groups.extend(user_groups.clone()); found_users.insert(user.clone(), groups); } - assert!(found_users.contains_key("user1")); + assert!(found_users.contains_key(&user1_name)); assert!(found_users - .get("user1") + .get(&user1_name) .unwrap() - .contains(format!("cn={},ou=groups,{}", "group-one", base_dn).as_str())); - assert!(found_users.contains_key("user2")); + .contains(format!("cn={},ou=groups,{}", &group1_name, base_dn).as_str())); + assert!(found_users.contains_key(&user2_name)); assert!(found_users - .get("user2") + .get(&user2_name) .unwrap() - .contains(format!("cn={},ou=groups,{}", "group-one", base_dn).as_str())); + .contains(format!("cn={},ou=groups,{}", &group1_name, base_dn).as_str())); assert!(found_users - .get("user2") + .get(&user2_name) .unwrap() - .contains(format!("cn={},ou=groups,{}", "group-two", base_dn).as_str())); - assert!(found_users.contains_key("user3")); - assert!(found_users.get("user3").unwrap().is_empty()); + .contains(format!("cn={},ou=groups,{}", &group2_name, base_dn).as_str())); + assert!(found_users.contains_key(&user3_name)); + assert!(found_users.get(&user3_name).unwrap().is_empty()); ldap.unbind().expect("failed to unbind ldap connection"); }