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.me...@example.com" 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

Reply via email to