diff --git a/crates/directory/src/backend/ldap/config.rs b/crates/directory/src/backend/ldap/config.rs index c6317610..41a6c73d 100644 --- a/crates/directory/src/backend/ldap/config.rs +++ b/crates/directory/src/backend/ldap/config.rs @@ -12,7 +12,9 @@ use utils::config::{Config, utils::AsKey}; use crate::core::config::build_pool; -use super::{AuthBind, Bind, LdapConnectionManager, LdapDirectory, LdapFilter, LdapMappings}; +use super::{ + AuthBind, Bind, LdapConnectionManager, LdapDirectory, LdapFilter, LdapFilterItem, LdapMappings, +}; impl LdapDirectory { pub fn from_config(config: &mut Config, prefix: impl AsKey, data_store: Store) -> Option { @@ -104,17 +106,25 @@ impl LdapDirectory { mappings.attrs_principal.extend(attr.iter().cloned()); } - let auth_bind = if config - .property_or_default::((&prefix, "bind.auth.enable"), "false") - .unwrap_or_default() + let auth_bind = match config + .value((&prefix, "bind.auth.method")) + .unwrap_or("default") { - let filter = LdapFilter::from_config(config, (&prefix, "bind.auth.dn")); - let search = config - .property_or_default::((&prefix, "bind.auth.search"), "true") - .unwrap_or(true); - Some(AuthBind { filter, search }) - } else { - None + "template" => AuthBind::Template { + template: LdapFilter::from_config(config, (&prefix, "bind.auth.template")), + can_search: config + .property_or_default::((&prefix, "bind.auth.search"), "true") + .unwrap_or(true), + }, + "lookup" => AuthBind::Lookup, + "default" => AuthBind::None, + unknown => { + config.new_parse_error( + (&prefix, "bind.auth.method"), + format!("Unknown LDAP bind method: {unknown}"), + ); + return None; + } }; Some(LdapDirectory { @@ -133,15 +143,60 @@ impl LdapDirectory { impl LdapFilter { fn from_config(config: &mut Config, key: impl AsKey) -> Self { if let Some(value) = config.value(key.clone()) { - let filter = LdapFilter { - filter: value.split('?').map(|s| s.to_string()).collect(), - }; - if filter.filter.len() >= 2 { - return filter; + let mut filter = Vec::new(); + let mut token = String::new(); + let mut value = value.chars(); + + while let Some(ch) = value.next() { + match ch { + '?' => { + // For backwards compatibility, we treat '?' as a placeholder for the full value. + if !token.is_empty() { + filter.push(LdapFilterItem::Static(token)); + token = String::new(); + } + filter.push(LdapFilterItem::Full); + } + '{' => { + if !token.is_empty() { + filter.push(LdapFilterItem::Static(token)); + token = String::new(); + } + for ch in value.by_ref() { + if ch == '}' { + break; + } else { + token.push(ch); + } + } + match token.as_str() { + "user" | "username" | "email" => filter.push(LdapFilterItem::Full), + "local" => filter.push(LdapFilterItem::LocalPart), + "domain" => filter.push(LdapFilterItem::DomainPart), + _ => { + config.new_parse_error( + key, + format!("Unknown LDAP filter placeholder: {}", token), + ); + return Self::default(); + } + } + token.clear(); + } + _ => token.push(ch), + } + } + + if !token.is_empty() { + filter.push(LdapFilterItem::Static(token)); + } + + if filter.len() >= 2 { + return LdapFilter { filter }; } else { config.new_parse_error( key, - format!("Missing '?' parameter placeholder in value {:?}", value), + format!("Missing parameter placeholders in value {:?}", value), ); } } diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs index d222a72e..fd72efc6 100644 --- a/crates/directory/src/backend/ldap/lookup.rs +++ b/crates/directory/src/backend/ldap/lookup.rs @@ -20,7 +20,7 @@ use crate::{ }, }; -use super::{LdapDirectory, LdapMappings}; +use super::{AuthBind, LdapDirectory, LdapMappings}; impl LdapDirectory { pub async fn query( @@ -29,16 +29,18 @@ impl LdapDirectory { return_member_of: bool, ) -> trc::Result> { let mut conn = self.pool.get().await.map_err(|err| err.into_error())?; - let (mut external_principal, member_of, stored_principal) = match by { QueryBy::Name(username) => { - if let Some((mut principal, member_of)) = self - .find_principal(&mut conn, &self.mappings.filter_name.build(username)) - .await? - { - principal.name = username.into(); - (principal, member_of, None) + let filter = self.mappings.filter_name.build(username); + if let Some(mut result) = self.find_principal(&mut conn, &filter).await? { + result.principal.name = username.into(); + (result.principal, result.member_of, None) } else { + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Name filter yielded no results", + Details = filter + ); return Ok(None); } } @@ -48,14 +50,14 @@ impl LdapDirectory { .query(QueryBy::Id(uid), return_member_of) .await? { - if let Some((principal, member_of)) = self + if let Some(result) = self .find_principal( &mut conn, &self.mappings.filter_name.build(stored_principal_.name()), ) .await? { - (principal, member_of, Some(stored_principal_)) + (result.principal, result.member_of, Some(stored_principal_)) } else { return Ok(None); } @@ -70,65 +72,138 @@ impl LdapDirectory { Credentials::XOauth2 { username, secret } => (username, secret), }; - if let Some(auth_bind) = &self.auth_bind { - let (auth_bind_conn, mut ldap) = LdapConnAsync::with_settings( - self.pool.manager().settings.clone(), - &self.pool.manager().address, - ) - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))?; - - ldap3::drive!(auth_bind_conn); - - let dn = auth_bind.filter.build(username); - - trc::event!(Store(trc::StoreEvent::LdapBind), Details = dn.clone()); - - if ldap - .simple_bind(&dn, secret) + match &self.auth_bind { + AuthBind::Template { + template, + can_search, + } => { + let (auth_bind_conn, mut ldap) = LdapConnAsync::with_settings( + self.pool.manager().settings.clone(), + &self.pool.manager().address, + ) .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .success() - .is_err() - { - return Ok(None); - } + .map_err(|err| err.into_error().caused_by(trc::location!()))?; - let filter = &self.mappings.filter_name.build(username); - let principal = if auth_bind.search { - self.find_principal(&mut ldap, filter).await - } else { - self.find_principal(&mut conn, filter).await - }; - match principal { - Ok(Some((mut principal, member_of))) => { - principal.name = username.into(); - (principal, member_of, None) - } - Err(err) - if err.matches(trc::EventType::Store(trc::StoreEvent::LdapError)) - && err - .value(trc::Key::Code) - .and_then(|v| v.to_uint()) - .is_some_and(|rc| [49, 50].contains(&rc)) => + ldap3::drive!(auth_bind_conn); + + let dn = template.build(username); + + if ldap + .simple_bind(&dn, secret) + .await + .map_err(|err| err.into_error().caused_by(trc::location!()))? + .success() + .is_err() { + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Secret rejected during auth bind using template", + Details = dn + ); return Ok(None); } - Ok(None) => return Ok(None), - Err(err) => return Err(err), + + let filter = self.mappings.filter_name.build(username); + let result = if *can_search { + self.find_principal(&mut ldap, &filter).await + } else { + self.find_principal(&mut conn, &filter).await + }; + + match result { + Ok(Some(mut result)) => { + result.principal.name = username.into(); + (result.principal, result.member_of, None) + } + Err(err) + if err + .matches(trc::EventType::Store(trc::StoreEvent::LdapError)) + && err + .value(trc::Key::Code) + .and_then(|v| v.to_uint()) + .is_some_and(|rc| [49, 50].contains(&rc)) => + { + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Error codes 49 or 50 returned by LDAP server", + Details = vec![dn, filter] + ); + return Ok(None); + } + Ok(None) => { + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Auth bind successful but filter yielded no results", + Details = vec![dn, filter] + ); + + return Ok(None); + } + Err(err) => return Err(err), + } } - } else if let Some((mut principal, member_of)) = self - .find_principal(&mut conn, &self.mappings.filter_name.build(username)) - .await? - { - if principal.verify_secret(secret).await? { - principal.name = username.into(); - (principal, member_of, None) - } else { - return Ok(None); + AuthBind::Lookup => { + let filter = self.mappings.filter_name.build(username); + if let Some(mut result) = self.find_principal(&mut conn, &filter).await? { + // Perform bind auth using the found dn + let (auth_bind_conn, mut ldap) = LdapConnAsync::with_settings( + self.pool.manager().settings.clone(), + &self.pool.manager().address, + ) + .await + .map_err(|err| err.into_error().caused_by(trc::location!()))?; + + ldap3::drive!(auth_bind_conn); + + if ldap + .simple_bind(&result.dn, secret) + .await + .map_err(|err| err.into_error().caused_by(trc::location!()))? + .success() + .is_ok() + { + result.principal.name = username.into(); + (result.principal, result.member_of, None) + } else { + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Secret rejected during auth bind using lookup filter", + Details = vec![result.dn, filter] + ); + return Ok(None); + } + } else { + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Auth bind lookup filter yielded no results", + Details = filter + ); + return Ok(None); + } + } + AuthBind::None => { + let filter = self.mappings.filter_name.build(username); + if let Some(mut result) = self.find_principal(&mut conn, &filter).await? { + if result.principal.verify_secret(secret).await? { + result.principal.name = username.into(); + (result.principal, result.member_of, None) + } else { + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Password verification failed", + Details = vec![result.dn, filter] + ); + return Ok(None); + } + } else { + trc::event!( + Store(trc::StoreEvent::LdapWarning), + Reason = "Authentication filter yielded no results", + Details = filter + ); + return Ok(None); + } } - } else { - return Ok(None); } } }; @@ -315,7 +390,7 @@ impl LdapDirectory { &self, conn: &mut Ldap, filter: &str, - ) -> trc::Result)>> { + ) -> trc::Result> { conn.search( &self.mappings.base_dn, Scope::Subtree, @@ -341,8 +416,14 @@ impl LdapDirectory { } } +struct LdapResult { + dn: String, + principal: Principal, + member_of: Vec, +} + impl LdapMappings { - fn entry_to_principal(&self, entry: SearchEntry) -> (Principal, Vec) { + fn entry_to_principal(&self, entry: SearchEntry) -> LdapResult { let mut principal = Principal::new(0, Type::Individual); let mut role = ROLE_USER; let mut member_of = vec![]; @@ -411,15 +492,21 @@ impl LdapMappings { } principal.data.push(PrincipalData::Roles(vec![role])); - (principal, member_of) + + LdapResult { + dn: entry.dn, + principal, + member_of, + } } } fn result_to_trace(rs: &ResultEntry) -> trc::Value { - SearchEntry::construct(rs.clone()) - .attrs + let se = SearchEntry::construct(rs.clone()); + se.attrs .into_iter() .map(|(k, v)| trc::Value::Array(vec![trc::Value::from(k), trc::Value::from(v.join(", "))])) + .chain([trc::Value::from(se.dn)]) .collect::>() .into() } diff --git a/crates/directory/src/backend/ldap/mod.rs b/crates/directory/src/backend/ldap/mod.rs index 1dee4817..c9f5f1ae 100644 --- a/crates/directory/src/backend/ldap/mod.rs +++ b/crates/directory/src/backend/ldap/mod.rs @@ -15,7 +15,7 @@ pub mod pool; pub struct LdapDirectory { pool: Pool, mappings: LdapMappings, - auth_bind: Option, + auth_bind: AuthBind, pub(crate) data_store: Store, } @@ -37,14 +37,40 @@ pub struct LdapMappings { } #[derive(Debug, Default)] -struct LdapFilter { - filter: Vec, +pub(crate) struct LdapFilter { + filter: Vec, +} + +#[derive(Debug)] +enum LdapFilterItem { + Static(String), + Full, + LocalPart, + DomainPart, } impl LdapFilter { pub fn build(&self, value: &str) -> String { - let value = ldap_escape(value); - self.filter.join(value.as_ref()) + let mut result = String::with_capacity(value.len() + 16); + + for item in &self.filter { + match item { + LdapFilterItem::Static(s) => result.push_str(s), + LdapFilterItem::Full => result.push_str(ldap_escape(value).as_ref()), + LdapFilterItem::LocalPart => { + if let Some((value, _)) = value.rsplit_once('@') { + result.push_str(value); + } + } + LdapFilterItem::DomainPart => { + if let Some((_, domain)) = value.rsplit_once('@') { + result.push_str(domain); + } + } + } + } + + result } } @@ -75,7 +101,11 @@ impl Bind { } } -pub(crate) struct AuthBind { - filter: LdapFilter, - search: bool, +pub(crate) enum AuthBind { + Template { + template: LdapFilter, + can_search: bool, + }, + Lookup, + None, } diff --git a/crates/directory/src/backend/ldap/pool.rs b/crates/directory/src/backend/ldap/pool.rs index 848e528d..a6f4d032 100644 --- a/crates/directory/src/backend/ldap/pool.rs +++ b/crates/directory/src/backend/ldap/pool.rs @@ -21,14 +21,6 @@ impl managed::Manager for LdapConnectionManager { ldap3::drive!(conn); - trc::event!( - Store(trc::StoreEvent::LdapBind), - Details = self - .bind_dn - .as_ref() - .map(|b| trc::Value::String(b.dn.as_str().into())), - ); - if let Some(bind) = &self.bind_dn { ldap.simple_bind(&bind.dn, &bind.password) .await? diff --git a/crates/trc/src/event/description.rs b/crates/trc/src/event/description.rs index 275ba124..8df5498d 100644 --- a/crates/trc/src/event/description.rs +++ b/crates/trc/src/event/description.rs @@ -1547,7 +1547,7 @@ impl StoreEvent { StoreEvent::BlobMissingMarker => "Blob missing marker", StoreEvent::SqlQuery => "SQL query executed", StoreEvent::LdapQuery => "LDAP query executed", - StoreEvent::LdapBind => "LDAP bind operation", + StoreEvent::LdapWarning => "LDAP authentication warning", StoreEvent::DataWrite => "Write batch operation", StoreEvent::BlobRead => "Blob read operation", StoreEvent::BlobWrite => "Blob write operation", @@ -1588,7 +1588,7 @@ impl StoreEvent { StoreEvent::BlobMissingMarker => "The blob is missing a marker", StoreEvent::SqlQuery => "An SQL query was executed", StoreEvent::LdapQuery => "An LDAP query was executed", - StoreEvent::LdapBind => "An LDAP bind operation was executed", + StoreEvent::LdapWarning => "An LDAP authentication warning occurred", StoreEvent::DataWrite => "A write batch operation was executed", StoreEvent::BlobRead => "A blob read operation was executed", StoreEvent::BlobWrite => "A blob write operation was executed", diff --git a/crates/trc/src/event/level.rs b/crates/trc/src/event/level.rs index 671851ef..b250b10a 100644 --- a/crates/trc/src/event/level.rs +++ b/crates/trc/src/event/level.rs @@ -18,14 +18,14 @@ impl EventType { | StoreEvent::BlobWrite | StoreEvent::BlobDelete | StoreEvent::SqlQuery - | StoreEvent::LdapQuery - | StoreEvent::LdapBind => Level::Trace, + | StoreEvent::LdapQuery => Level::Trace, StoreEvent::CacheMiss | StoreEvent::CacheHit | StoreEvent::CacheStale | StoreEvent::CacheUpdate | StoreEvent::NotFound - | StoreEvent::HttpStoreFetch => Level::Debug, + | StoreEvent::HttpStoreFetch + | StoreEvent::LdapWarning => Level::Debug, StoreEvent::AssertValueFailed | StoreEvent::FoundationdbError | StoreEvent::MysqlError diff --git a/crates/trc/src/lib.rs b/crates/trc/src/lib.rs index 8abaa68f..437bbd43 100644 --- a/crates/trc/src/lib.rs +++ b/crates/trc/src/lib.rs @@ -857,7 +857,7 @@ pub enum StoreEvent { BlobDelete, SqlQuery, LdapQuery, - LdapBind, + LdapWarning, HttpStoreFetch, } diff --git a/crates/trc/src/serializers/binary.rs b/crates/trc/src/serializers/binary.rs index f109c10a..ec39e3c1 100644 --- a/crates/trc/src/serializers/binary.rs +++ b/crates/trc/src/serializers/binary.rs @@ -816,7 +816,7 @@ impl EventType { EventType::Store(StoreEvent::ElasticsearchError) => 516, EventType::Store(StoreEvent::FilesystemError) => 517, EventType::Store(StoreEvent::FoundationdbError) => 518, - EventType::Store(StoreEvent::LdapBind) => 519, + EventType::Store(StoreEvent::LdapWarning) => 519, EventType::Store(StoreEvent::LdapError) => 520, EventType::Store(StoreEvent::LdapQuery) => 521, EventType::Store(StoreEvent::MysqlError) => 522, @@ -1435,7 +1435,7 @@ impl EventType { 516 => Some(EventType::Store(StoreEvent::ElasticsearchError)), 517 => Some(EventType::Store(StoreEvent::FilesystemError)), 518 => Some(EventType::Store(StoreEvent::FoundationdbError)), - 519 => Some(EventType::Store(StoreEvent::LdapBind)), + 519 => Some(EventType::Store(StoreEvent::LdapWarning)), 520 => Some(EventType::Store(StoreEvent::LdapError)), 521 => Some(EventType::Store(StoreEvent::LdapQuery)), 522 => Some(EventType::Store(StoreEvent::MysqlError)), diff --git a/tests/resources/ldap/ldap.cfg b/tests/resources/ldap/ldap.cfg index ad8945ea..753eb9ec 100644 --- a/tests/resources/ldap/ldap.cfg +++ b/tests/resources/ldap/ldap.cfg @@ -74,6 +74,7 @@ watchconfig = true sn = "info@example.org" mail = "bill@example.org" uidnumber = 4 + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" [[users.customattributes]] principalName = ["Bill Foobar"] diskQuota = [500000] diff --git a/tests/src/directory/ldap.rs b/tests/src/directory/ldap.rs index 2471cfac..6d59483e 100644 --- a/tests/src/directory/ldap.rs +++ b/tests/src/directory/ldap.rs @@ -19,12 +19,7 @@ use crate::directory::{ #[tokio::test] async fn ldap_directory() { // Enable logging - /*tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_max_level(tracing::Level::DEBUG) - .finish(), - ) - .unwrap();*/ + crate::enable_logging(); // Obtain directory handle let mut config = DirectoryTest::new("sqlite".into()).await; @@ -33,77 +28,100 @@ async fn ldap_directory() { let core = config.server; // Test authentication - assert_eq!( - handle - .query( - QueryBy::Credentials(&Credentials::Plain { - username: "john".into(), - secret: "12345".into() - }), - true - ) - .await - .unwrap() - .unwrap() - .into_test() - .into_sorted(), - TestPrincipal { - id: base_store.get_principal_id("john").await.unwrap().unwrap(), - name: "john".into(), - description: Some("John Doe".into()), - secrets: vec!["12345".into()], - typ: Type::Individual, - member_of: map_account_ids(base_store, vec!["sales"]) + for (auth_type, handle) in [ + ("Default", handle.clone()), + ( + "Bind template", + config + .directories + .directories + .remove("ldap-bind-template") + .unwrap(), + ), + ( + "Bind lookup", + config + .directories + .directories + .remove("ldap-bind-lookup") + .unwrap(), + ), + ] { + println!("Testing {auth_type} LDAP authentication..."); + assert_eq!( + handle + .query( + QueryBy::Credentials(&Credentials::Plain { + username: "john".into(), + secret: "12345".into() + }), + true + ) .await - .into_iter() - .map(|v| v.to_string()) - .collect(), - emails: vec!["john@example.org".into(), "john.doe@example.org".into()], - roles: vec![ROLE_USER.to_string()], - ..Default::default() - } - .into_sorted() - ); - assert_eq!( - handle - .query( - QueryBy::Credentials(&Credentials::Plain { - username: "bill".into(), - secret: "password".into() - }), - true - ) - .await - .unwrap() - .unwrap() - .into_test() - .into_sorted(), - TestPrincipal { - id: base_store.get_principal_id("bill").await.unwrap().unwrap(), - name: "bill".into(), - description: Some("Bill Foobar".into()), - secrets: vec!["$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe".into()], - typ: Type::Individual, - quota: 500000, - emails: vec!["bill@example.org".into(),], - roles: vec![ROLE_USER.to_string()], - ..Default::default() - } - .into_sorted() - ); - assert!( - handle - .query( - QueryBy::Credentials(&Credentials::Plain { - username: "bill".into(), - secret: "invalid".into() - }), - true - ) - .await - .unwrap() - .is_none() - ); + .unwrap() + .unwrap() + .into_test() + .into_sorted(), + TestPrincipal { + id: base_store.get_principal_id("john").await.unwrap().unwrap(), + name: "john".into(), + description: Some("John Doe".into()), + secrets: vec!["12345".into()], + typ: Type::Individual, + member_of: map_account_ids(base_store, vec!["sales"]) + .await + .into_iter() + .map(|v| v.to_string()) + .collect(), + emails: vec!["john@example.org".into(), "john.doe@example.org".into()], + roles: vec![ROLE_USER.to_string()], + ..Default::default() + } + .into_sorted() + ); + assert_eq!( + handle + .query( + QueryBy::Credentials(&Credentials::Plain { + username: "bill".into(), + secret: "password".into() + }), + true + ) + .await + .unwrap() + .unwrap() + .into_test() + .into_sorted(), + TestPrincipal { + id: base_store.get_principal_id("bill").await.unwrap().unwrap(), + name: "bill".into(), + description: Some("Bill Foobar".into()), + secrets: vec![ + "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe".into() + ], + typ: Type::Individual, + quota: 500000, + emails: vec!["bill@example.org".into(),], + roles: vec![ROLE_USER.to_string()], + ..Default::default() + } + .into_sorted() + ); + assert!( + handle + .query( + QueryBy::Credentials(&Credentials::Plain { + username: "bill".into(), + secret: "invalid".into() + }), + true + ) + .await + .unwrap() + .is_none() + ); + } // Get user by name assert_eq!( diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index c7aca6de..16442b43 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -143,19 +143,9 @@ base-dn = "dc=example,dc=org" dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=org" secret = "mysecret" -[directory."ldap".bind.auth] -enable = false -dn = "cn=?,ou=svcaccts,dc=example,dc=org" - [directory."ldap".filter] name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(uid=?))" email = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=?)(givenName=?)(sn=?)))" -verify = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=*?*)(givenName=*?*)))" -expand = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(sn=?))" -domains = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=*@?)(givenName=*@?)(sn=*@?)))" - -# Glauth does not support searchable custom attributes so -# 'sn' and 'givenName' are used to search for aliases/lists. [directory."ldap".attributes] name = "uid" @@ -167,6 +157,60 @@ email-alias = "givenName" quota = "diskQuota" class = "objectClass" +[directory."ldap-bind-template"] +type = "ldap" +url = "ldap://localhost:3893" +base-dn = "dc=example,dc=org" + +[directory."ldap-bind-template".bind] +dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=org" +secret = "mysecret" + +[directory."ldap-bind-template".bind.auth] +method = "template" +template = "cn={username},ou=,dc=example,dc=org" +search = false + +[directory."ldap-bind-template".filter] +name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(uid=?))" +email = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=?)(givenName=?)(sn=?)))" + +[directory."ldap-bind-template".attributes] +name = "uid" +description = ["principalName", "description"] +secret = "userPassword" +groups = ["memberOf", "otherGroups"] +email = "mail" +email-alias = "givenName" +quota = "diskQuota" +class = "objectClass" + +[directory."ldap-bind-lookup"] +type = "ldap" +url = "ldap://localhost:3893" +base-dn = "dc=example,dc=org" + +[directory."ldap-bind-lookup".bind] +dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=org" +secret = "mysecret" + +[directory."ldap-bind-lookup".bind.auth] +method = "lookup" + +[directory."ldap-bind-lookup".filter] +name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(uid=?))" +email = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=?)(givenName=?)(sn=?)))" + +[directory."ldap-bind-lookup".attributes] +name = "uid" +description = ["principalName", "description"] +secret = "userPassword" +groups = ["memberOf", "otherGroups"] +email = "mail" +email-alias = "givenName" +quota = "diskQuota" +class = "objectClass" + ############################################################################## [directory."imap"]