Hi there,
I've previously (a long time ago, actually, too long if you ask me) made
inquiries as to who might be using ptclient/ldap.c[1,2], and in which
fashion; I got three points from the responses;
- Everything should be configurable as LDAP deployments typically vary
widely and often pre-date a Cyrus IMAP deployment[3],
- It should handle groups better[4], namely nested/recursive groups,
the 'memberOf' attribute,
- It should handle memberUrls[5].
While these are all valid points and worth working on for me as well,
today I have another aspect; the handling of multi-domain deployments,
with isolated root dns for each parent domain. A very ugly and
presumptuous patch is attached, that needs extra careful checking and a
nice cleanup.
This scenario (of multiple domains separated in to multiple, different
root DNs) is widely in use with Kolab Groupware, while canonification
nor group ACLs would work.
The scenario for such a deployment could be described as follows:
- A list of objectClass=domainRelatedObject LDAP objects exists in
cn=kolab,cn=config.
- A domain "example.org" may have a root dn of "dc=example,dc=org",
and would be an LDAP entry as follows:
dn: associatedDomain=example.org,cn=kolab,cn=config
objectClass: top
objectClass: domainRelatedObject
associatedDomain: example.org
- To translate "example.org" to "dc=example,dc=org" in this particular
case, the C equivalent of:
$root_dn = 'dc=' . implode(',dc=', explode('.', "example.org"));
can be used.
- Another domain "example.com" mayhave a root dn of "o=example,c=de",
and could be an LDAP entry as follows:
dn: associatedDomain=example.com,cn=kolab,cn=config
objectClass: top
objectClass: domainRelatedObject
objectClass: inetDomain
associatedDomain: example.com
inetDomainBaseDN: o=example,dc=de
- Here, the inetDomainBaseDN attribute should be used to translate a
user login of "lucy.meier@xxxxxxxxxxx" to a search against
"o=example,dc=de".
This leads me to believe the following items should be configurable:
- domain_base_dn
The base dn to use when searching for "domains" or "domain name
spaces".
For Kolab Groupware deployments, this is a default of
"cn=kolab,cn=config", but could of course be
"ou=Domains,dc=domain,dc=tld" as well.
- domain_filter
The filter to use.
Perhaps something like
"(&(objectClass=domainRelatedObject)(inetDomainStatus=on)(associatedDomain=%s))"
- domain_scope
The search scope. "sub", "one" or "base", with a default of "sub".
- domain_name_attribute
The attribute name for actual domain name spaces, such as
"associatedDomain".
For LDAP deployments without the Netscape schemas I suppose this
attribute name might be "cn".
For situations in which a domainRelatedObject does not contain one,
but multiple values for the domain_name_attribute, the first value
returned by LDAP is used (this typically corresponds with the relative
DN attribute value, and should be consistent).
- domain_result_attribute
Name of the attribute to look for, for example "inetDomainBaseDN".
If no such attribute exists (for the object found), fall back to the
"standard" root dn described above (the implode over explode thing).
I would appreciate your thoughts and feedback.
Thanks, in advance,
Kind regards,
Jeroen van Meeuwen
[1]
http://lists.andrew.cmu.edu/pipermail/cyrus-devel/2011-August/001923.html
[2]
http://lists.andrew.cmu.edu/pipermail/info-cyrus/2011-August/035257.html
[3]
http://lists.andrew.cmu.edu/pipermail/info-cyrus/2011-August/035259.html
[4]
http://lists.andrew.cmu.edu/pipermail/info-cyrus/2011-August/035262.html
[5]
http://lists.andrew.cmu.edu/pipermail/info-cyrus/2011-August/035294.html
--
Systems Architect, Kolab Systems AG
e: vanmeeuwen at kolabsys.com
m: +44 74 2516 3817
w: http://www.kolabsys.com
pgp: 9342 BF08
diff --git a/lib/imapoptions b/lib/imapoptions
index ecb54ef..0725fd9 100644
--- a/lib/imapoptions
+++ b/lib/imapoptions
@@ -597,6 +597,21 @@ Blank lines and lines beginning with ``#'' are ignored.
{ "ldap_deref", "never", STRINGLIST("search", "find", "always", "never") }
/* Specify how aliases dereferencing is handled during search. */
+{ "ldap_domain_base_dn", "", STRING }
+/* Base DN to search for domain name spaces. */
+
+{ "ldap_domain_filter", "(&(objectclass=domainrelatedobject)(associateddomain=%s))", STRING }
+/* Filter to use searching for domains */
+
+{ "ldap_domain_name_attribute", "associateddomain", STRING }
+/* The attribute name for domains. */
+
+{ "ldap_domain_scope", "sub", STRINGLIST("sub", "one", "base") }
+/* Search scope */
+
+{ "ldap_domain_result_attribute", "inetdomainbasedn", STRING }
+/* Result attribute */
+
{ "ldap_filter", "(uid=%u)", STRING }
/* Specify a filter that searches user identifiers. The following tokens can be
used in the filter string:
diff --git a/ptclient/ldap.c b/ptclient/ldap.c
index beb31d9..e16e74a 100644
--- a/ptclient/ldap.c
+++ b/ptclient/ldap.c
@@ -131,42 +131,50 @@ static char allowedchars[256] = {
};
typedef struct _ptsm {
- const char *uri;
- int version;
- struct timeval timeout;
- int size_limit;
- int time_limit;
- int deref;
- int referrals;
- int restart;
- int scope;
- const char *base;
- int sasl;
- const char *id;
- const char *bind_dn;
- const char *password;
- const char *authz;
- const char *mech;
- const char *realm;
- const char *filter;
- const char *sasl_secprops;
- int start_tls;
- int tls_check_peer;
- const char *tls_cacert_file;
- const char *tls_cacert_dir;
- const char *tls_ciphers;
- const char *tls_cert;
- const char *tls_key;
- int member_method;
- const char *user_attribute;
- const char *member_attribute;
- const char *member_filter;
- const char *member_base;
- int member_scope;
- const char *group_filter;
- const char *group_base;
- int group_scope;
- LDAP *ld;
+ const char *uri;
+ int version;
+ struct timeval timeout;
+ int size_limit;
+ int time_limit;
+ int deref;
+ int referrals;
+ int restart;
+ int scope;
+ const char *base;
+ int sasl;
+ const char *id;
+ const char *bind_dn;
+ const char *password;
+ const char *authz;
+ const char *mech;
+ const char *realm;
+ const char *filter;
+ const char *sasl_secprops;
+ int start_tls;
+ int tls_check_peer;
+ const char *tls_cacert_file;
+ const char *tls_cacert_dir;
+ const char *tls_ciphers;
+ const char *tls_cert;
+ const char *tls_key;
+ int member_method;
+ const char *user_attribute;
+ const char *member_attribute;
+ const char *member_filter;
+ const char *member_base;
+ int member_scope;
+ const char *group_filter;
+ const char *group_base;
+ int group_scope;
+
+ /* Used for domain name space -> root dn discovery */
+ const char *domain_base_dn;
+ const char *domain_filter;
+ const char *domain_name_attribute;
+ int domain_scope;
+ const char *domain_result_attribute;
+
+ LDAP *ld;
} t_ptsm;
#define PTSM_OK 0
@@ -448,11 +456,14 @@ static void myinit(void)
ptsm->uri = (config_getstring(IMAPOPT_LDAP_URI) ?
config_getstring(IMAPOPT_LDAP_URI) : config_getstring(IMAPOPT_LDAP_SERVERS));
+
ptsm->version = (config_getint(IMAPOPT_LDAP_VERSION) == 2 ? LDAP_VERSION2 : LDAP_VERSION3);
ptsm->timeout.tv_sec = config_getint(IMAPOPT_LDAP_TIME_LIMIT);
ptsm->timeout.tv_usec = 0;
ptsm->restart = config_getswitch(IMAPOPT_LDAP_RESTART);
+
p = config_getstring(IMAPOPT_LDAP_DEREF);
+
if (!strcasecmp(p, "search")) {
ptsm->deref = LDAP_DEREF_SEARCHING;
} else if (!strcasecmp(p, "find")) {
@@ -462,10 +473,13 @@ static void myinit(void)
} else {
ptsm->deref = LDAP_DEREF_NEVER;
}
+
ptsm->referrals = config_getswitch(IMAPOPT_LDAP_REFERRALS);
ptsm->size_limit = config_getint(IMAPOPT_LDAP_SIZE_LIMIT);
ptsm->time_limit = config_getint(IMAPOPT_LDAP_TIME_LIMIT);
+
p = config_getstring(IMAPOPT_LDAP_SCOPE);
+
if (!strcasecmp(p, "one")) {
ptsm->scope = LDAP_SCOPE_ONELEVEL;
} else if (!strcasecmp(p, "base")) {
@@ -473,18 +487,24 @@ static void myinit(void)
} else {
ptsm->scope = LDAP_SCOPE_SUBTREE;
}
+
ptsm->bind_dn = config_getstring(IMAPOPT_LDAP_BIND_DN);
ptsm->sasl = config_getswitch(IMAPOPT_LDAP_SASL);
ptsm->id = (config_getstring(IMAPOPT_LDAP_ID) ?
config_getstring(IMAPOPT_LDAP_ID) : config_getstring(IMAPOPT_LDAP_SASL_AUTHC));
+
ptsm->authz = (config_getstring(IMAPOPT_LDAP_AUTHZ) ?
config_getstring(IMAPOPT_LDAP_AUTHZ) : config_getstring(IMAPOPT_LDAP_SASL_AUTHZ));
+
ptsm->mech = (config_getstring(IMAPOPT_LDAP_MECH) ?
config_getstring(IMAPOPT_LDAP_MECH) : config_getstring(IMAPOPT_LDAP_SASL_MECH));
+
ptsm->realm = (config_getstring(IMAPOPT_LDAP_REALM) ?
config_getstring(IMAPOPT_LDAP_REALM) : config_getstring(IMAPOPT_LDAP_SASL_REALM));
+
ptsm->password = (config_getstring(IMAPOPT_LDAP_PASSWORD) ?
config_getstring(IMAPOPT_LDAP_PASSWORD) : config_getstring(IMAPOPT_LDAP_SASL_PASSWORD));
+
ptsm->start_tls = config_getswitch(IMAPOPT_LDAP_START_TLS);
ptsm->tls_check_peer = config_getswitch(IMAPOPT_LDAP_TLS_CHECK_PEER);
ptsm->tls_cacert_file = config_getstring(IMAPOPT_LDAP_TLS_CACERT_FILE);
@@ -492,12 +512,14 @@ static void myinit(void)
ptsm->tls_ciphers = config_getstring(IMAPOPT_LDAP_TLS_CIPHERS);
ptsm->tls_cert = config_getstring(IMAPOPT_LDAP_TLS_CERT);
ptsm->tls_key = config_getstring(IMAPOPT_LDAP_TLS_KEY);
+
p = config_getstring(IMAPOPT_LDAP_MEMBER_METHOD);
if (!strcasecmp(p, "filter")) {
ptsm->member_method = PTSM_MEMBER_METHOD_FILTER;
} else {
ptsm->member_method = PTSM_MEMBER_METHOD_ATTRIBUTE;
}
+
p = config_getstring(IMAPOPT_LDAP_MEMBER_SCOPE);
if (!strcasecmp(p, "one")) {
ptsm->member_scope = LDAP_SCOPE_ONELEVEL;
@@ -506,12 +528,15 @@ static void myinit(void)
} else {
ptsm->member_scope = LDAP_SCOPE_SUBTREE;
}
+
ptsm->member_filter = config_getstring(IMAPOPT_LDAP_MEMBER_FILTER);
ptsm->member_base = config_getstring(IMAPOPT_LDAP_MEMBER_BASE);
ptsm->member_attribute = (config_getstring(IMAPOPT_LDAP_MEMBER_ATTRIBUTE) ?
config_getstring(IMAPOPT_LDAP_MEMBER_ATTRIBUTE) : config_getstring(IMAPOPT_LDAP_MEMBER_ATTRIBUTE));
+
ptsm->user_attribute = (config_getstring(IMAPOPT_LDAP_USER_ATTRIBUTE) ?
config_getstring(IMAPOPT_LDAP_USER_ATTRIBUTE) : config_getstring(IMAPOPT_LDAP_USER_ATTRIBUTE));
+
p = config_getstring(IMAPOPT_LDAP_GROUP_SCOPE);
if (!strcasecmp(p, "one")) {
ptsm->group_scope = LDAP_SCOPE_ONELEVEL;
@@ -520,16 +545,31 @@ static void myinit(void)
} else {
ptsm->group_scope = LDAP_SCOPE_SUBTREE;
}
+
ptsm->group_filter = config_getstring(IMAPOPT_LDAP_GROUP_FILTER);
ptsm->group_base = config_getstring(IMAPOPT_LDAP_GROUP_BASE);
ptsm->filter = config_getstring(IMAPOPT_LDAP_FILTER);
ptsm->base = config_getstring(IMAPOPT_LDAP_BASE);
- if (ptsm->version != LDAP_VERSION3 &&
- (ptsm->sasl ||
- ptsm->start_tls))
+ if (ptsm->version != LDAP_VERSION3 && (ptsm->sasl || ptsm->start_tls))
ptsm->version = LDAP_VERSION3;
+ ptsm->domain_base_dn = config_getstring(IMAPOPT_LDAP_DOMAIN_BASE_DN);
+ ptsm->domain_filter = config_getstring(IMAPOPT_LDAP_DOMAIN_FILTER);
+ ptsm->domain_name_attribute = config_getstring(IMAPOPT_LDAP_DOMAIN_NAME_ATTRIBUTE);
+
+ p = config_getstring(IMAPOPT_LDAP_DOMAIN_SCOPE);
+
+ if (!strcasecmp(p, "one")) {
+ ptsm->domain_scope = LDAP_SCOPE_ONELEVEL;
+ } else if (!strcasecmp(p, "base")) {
+ ptsm->domain_scope = LDAP_SCOPE_BASE;
+ } else {
+ ptsm->domain_scope = LDAP_SCOPE_SUBTREE;
+ }
+
+ ptsm->domain_result_attribute = config_getstring(IMAPOPT_LDAP_DOMAIN_RESULT_ATTRIBUTE);
+
ptsm->ld = NULL;
}
@@ -588,6 +628,62 @@ static int ptsmodule_escape(
return PTSM_OK;
}
+static int *ptsmodule_standard_root_dn(const char *domain, const char **result)
+{
+ /* number of dots */
+ int dots;
+ /* the expected length of the result */
+ int root_dn_len;
+
+ char *buf;
+ char *part;
+ char *ptr;
+
+ syslog(LOG_DEBUG, "ptsmodule_standard_root_dn called for domain %s", domain);
+
+ for (dots = 0, buf=(char *)domain; *buf; buf++) {
+ if (*buf == '.') {
+ dots++;
+ }
+ }
+
+ /* Each dot is to be replaced with ',dc=' (length 4), so add
+ * length 3 for each of them.
+ */
+ root_dn_len = strlen(domain) + (dots * 3);
+
+ buf = xmalloc(root_dn_len);
+ buf[0] = '\0'; // (?)
+
+ part = strtok_r(xstrdup(domain), ".", &ptr);
+
+ while (part != NULL) {
+ syslog(LOG_DEBUG, "Root DN now %s", buf);
+ strcat(buf, ",dc=");
+ syslog(LOG_DEBUG, "Root DN now %s", buf);
+ strcat(buf, part);
+ syslog(LOG_DEBUG, "Root DN now %s", buf);
+ part = strtok_r(NULL, ".", &ptr);
+ }
+
+ syslog(LOG_DEBUG, "Root DN now %s", buf);
+
+ if (buf[0] == ',')
+ memmove(buf, buf+1, strlen(buf));
+
+ *result = xstrdup(buf);
+
+/* free(buf);
+ free(part);
+ free(ptr);
+ free(dots);
+ free(root_dn_len);
+*/
+ syslog(LOG_DEBUG, "Root DN now %s", xstrdup(*result));
+
+ return PTSM_OK;
+}
+
static int ptsmodule_tokenize_domains(
const char *d,
int n,
@@ -779,7 +875,6 @@ static int ptsmodule_expand_tokens(
return PTSM_OK;
}
-
static int ptsmodule_get_dn(
const char *canon_id,
size_t size,
@@ -794,11 +889,14 @@ static int ptsmodule_get_dn(
char *authzid;
#endif
char *base = NULL, *filter = NULL;
+ char *domain = NULL;
+ char domain_filter[1024];
char *attrs[] = {LDAP_NO_ATTRS,NULL}; //do not return all attrs!
+ char *domain_attrs[] = {(char *)ptsm->domain_name_attribute,(char *)ptsm->domain_result_attribute,NULL};
LDAPMessage *res;
LDAPMessage *entry;
- char *attr, **vals;
- BerElement *ber;
+ char **vals;
+ /* unused: BerElement *ber; */
*ret = NULL;
@@ -847,30 +945,122 @@ static int ptsmodule_get_dn(
if (rc != PTSM_OK)
return rc;
- rc = ptsmodule_expand_tokens(ptsm->base, canon_id, NULL, &base);
- if (rc != PTSM_OK)
- return rc;
+ if (ptsm->domain_base_dn && (strrchr(canon_id, '@') != NULL)) {
+ syslog(LOG_DEBUG, "Attempting to get domain for %s from %s", canon_id, ptsm->domain_base_dn);
+
+ /* Get the base dn to search from domain_base_dn searched on domain_scope with
+ domain_filter */
+ domain = strrchr(canon_id, '@');
+
+ syslog(LOG_DEBUG, "Input domain would be %s", domain);
+
+ /* Strip the first character which is a '@' */
+ memmove(domain, domain+1, strlen(domain));
+
+ syslog(LOG_DEBUG, "Input domain would be %s", domain);
+
+/* TODO: Find something meaningful for this.
+ rc = ptsmodule_expand_tokens(ptsm->domain_filter, domain, NULL, &domain_filter);
+
+ if (rc != PTSM_OK)
+ return rc;
+*/
+
+ snprintf(domain_filter, sizeof(domain_filter), ptsm->domain_filter, domain);
+
+ syslog(LOG_DEBUG, "Domain filter: %s", domain_filter);
+
+ rc = ldap_search_st(ptsm->ld, ptsm->domain_base_dn, ptsm->domain_scope, domain_filter, domain_attrs, 0, &(ptsm->timeout), &res);
+
+ if (rc != LDAP_SUCCESS) {
+ syslog(LOG_DEBUG, "Result from domain query not OK");
+ return rc;
+ } else {
+ syslog(LOG_DEBUG, "Result from domain query OK");
+ free(rc);
+ }
+
+ if (ldap_count_entries(ptsm->ld, res) < 1) {
+ syslog(LOG_ERR, "No domain %s found", domain);
+ return PTSM_FAIL;
+ } else if (ldap_count_entries(ptsm->ld, res) > 1) {
+ syslog(LOG_ERR, "Multiple domains %s found", domain);
+ return PTSM_FAIL;
+ } else {
+ syslog(LOG_DEBUG, "Domain %s found", domain);
+ if ((entry = ldap_first_entry(ptsm->ld, res)) != NULL) {
+ if ((vals = ldap_get_values(ptsm->ld, entry, ptsm->domain_result_attribute)) != NULL) {
+ ptsm->base = vals[0];
+ rc = PTSM_OK;
+ } else if ((vals = ldap_get_values(ptsm->ld, entry, ptsm->domain_name_attribute)) != NULL) {
+ char *new_domain = xstrdup(vals[0]);
+ syslog(LOG_DEBUG, "Domain %s is now domain %s", domain, new_domain);
+ rc = ptsmodule_standard_root_dn(new_domain, &ptsm->base);
+ free(new_domain);
+ } else {
+ rc = ptsmodule_standard_root_dn(domain, &ptsm->base);
+ }
+
+ if (rc != PTSM_OK) {
+ return rc;
+ } else {
+ base = xstrdup(ptsm->base);
+ syslog(LOG_DEBUG, "Continuing with ptsm->base: %s", ptsm->base);
+ }
+ }
+ }
+
+/* if (domain)
+ free(domain);
+
+ if (domain_filter)
+ free(domain_filter);
+*/
+ } else {
+ rc = ptsmodule_expand_tokens(ptsm->base, canon_id, NULL, &base);
+ if (rc != PTSM_OK)
+ return rc;
+ }
+
+ syslog(LOG_DEBUG, "about to search %s for %s", base, filter);
rc = ldap_search_st(ptsm->ld, base, ptsm->scope, filter, attrs, 0, &(ptsm->timeout), &res);
- free(filter);
- free(base);
+
if (rc != LDAP_SUCCESS) {
+ syslog(LOG_DEBUG, "actual search for %s on %s is not OK", filter, base);
+ free(filter);
+ free(base);
+
if (rc == LDAP_SERVER_DOWN) {
ldap_unbind(ptsm->ld);
ptsm->ld = NULL;
return PTSM_RETRY;
}
+
return PTSM_FAIL;
}
- /*
- * We don't want to return the *first* entry found, we want to return
- * the *only* entry found.
- */
- if ( ldap_count_entries(ptsm->ld, res) == 1 ) {
- if ( (entry = ldap_first_entry(ptsm->ld, res)) != NULL )
- *ret = ldap_get_dn(ptsm->ld, entry);
- }
+ syslog(LOG_DEBUG, "search for %s on %s OK", filter, base);
+
+ free(filter);
+ free(base);
+
+ /*
+ * We don't want to return the *first* entry found, we want to return
+ * the *only* entry found.
+ */
+ if (ldap_count_entries(ptsm->ld, res) < 1) {
+ syslog(LOG_WARNING, "No entries found");
+ } else if (ldap_count_entries(ptsm->ld, res) > 1) {
+ syslog(LOG_WARNING, "Multiple entries found: %d", ldap_count_entries(ptsm->ld, res));
+ } else {
+/* if ( ldap_count_entries(ptsm->ld, res) == 1 ) { */
+ if ((entry = ldap_first_entry(ptsm->ld, res)) != NULL) {
+ *ret = ldap_get_dn(ptsm->ld, entry);
+ } else {
+ syslog(LOG_DEBUG, "entry is null?");
+ }
+ }
ldap_msgfree(res);
res = NULL;
----
Cyrus Home Page: http://www.cyrusimap.org/
List Archives/Info: http://lists.andrew.cmu.edu/pipermail/info-cyrus/
To Unsubscribe:
https://lists.andrew.cmu.edu/mailman/listinfo/info-cyrus