/* * auth_ldap.c: * Authenticate users against a LDAP server. * * designed for tpop3d by Sebastien THOMAS (prune@lecentre.net) - Mad Cow tribe * Copyright (c) 2002 Sebastien Thomas, Chris Lightfoot. All rights reserved. * */ #ifdef HAVE_CONFIG_H #include "configuration.h" #endif /* HAVE_CONFIG_H */ #ifdef AUTH_LDAP static const char rcsid[] = "$Id: auth_ldap.c,v 1.16 2003/10/23 09:43:50 chris Exp $"; #include /* BSD needs this here, apparently. */ #include #include #include #include #include #include #include #include #include "auth_ldap.h" #include "authswitch.h" #include "config.h" #include "stringmap.h" #include "util.h" /* ldapinfo: * Information relating to the LDAP server and queries against same. */ static struct { char *hostname; int port; char *dn, *searchdn, *password; uid_t uid; gid_t gid; int tls; char *filter_spec; int scope; struct { char *mailbox, *mboxtype, *user, *group; } attr; LDAP *ldap; } ldapinfo = { NULL, /* no default host */ LDAP_PORT, /* default port */ NULL, /* dn */ NULL, /* no default search dn */ NULL, /* or password */ -1, -1, /* no default user/group */ 0, /* don't use TLS */ "(mail=$(local_part)@$(domain))", /* default filter matches complete email address * to mail attribute */ LDAP_SCOPE_SUBTREE, /* search subtree by default. */ { NULL, /* attribute from which to obtain mailbox location */ NULL, /* by default, guess mailbox type. */ NULL, /* user id */ NULL, /* group id */ }, NULL }; static char *substitute_filter_params(const char *template, const char *user, const char *local_part, const char *domain); /* auth_ldap_connect: * Try to connect to the LDAP server. */ static int auth_ldap_connect(void) { int r = 1; if (!(ldapinfo.ldap = ldap_open(ldapinfo.hostname, ldapinfo.port))) { log_print(LOG_ERR, "auth_ldap_connect: ldap_open: %m"); return 0; } if (ldapinfo.tls) { int vers, ret; vers = LDAP_VERSION3; if ((ret = ldap_set_option(ldapinfo.ldap, LDAP_OPT_PROTOCOL_VERSION, &vers)) != LDAP_OPT_SUCCESS) { log_print(LOG_ERR, "auth_ldap_connect: ldap_set_option(LDAP_VERSION3): %s", ldap_err2string(ret)); r = 0; } else if ((ret = ldap_start_tls_s(ldapinfo.ldap, NULL, NULL)) != LDAP_SUCCESS) { log_print(LOG_ERR, "auth_ldap_connect: ldap_start_tls_s: %s", ldap_err2string(ret)); r = 0; } } if (!r) { ldap_unbind(ldapinfo.ldap); ldapinfo.ldap = NULL; } return r; } /* auth_ldap_init: * Read configuration directives relating to LDAP and save them in the * ldapinfo structure. */ extern int verbose; /* in main.c */ int auth_ldap_init(void) { char *ldap_url = NULL, *s, *t; int ret = 0, r = 0; LDAPURLDesc *urldesc = NULL; /* get the data from an ldap_url string */ if (!(ldap_url = config_get_string("auth-ldap-url"))) { log_print(LOG_ERR, _("auth_ldap_init: no auth-ldap-url directive in config")); goto fail; } /* Find hostname and port from ldap url */ if ((ret = ldap_url_parse(ldap_url, &urldesc)) != LDAP_URL_SUCCESS) { log_print(LOG_ERR, _("auth_ldap_init: %s: URL error %d"), ldap_url, ret); goto fail; } ldapinfo.hostname = xstrdup(urldesc->lud_host); /* If no port is specified, use the default. */ if (urldesc->lud_port) ldapinfo.port = urldesc->lud_port; if (!(ldapinfo.port = urldesc->lud_port)) ldapinfo.port = LDAP_PORT; if (urldesc->lud_dn) ldapinfo.dn = xstrdup(urldesc->lud_dn); ldap_free_urldesc(urldesc); if (verbose) log_print(LOG_DEBUG, _("auth_ldap_init: using DN %s on %s:%d"), ldapinfo.dn ? ldapinfo.dn : "n/a", ldapinfo.hostname, ldapinfo.port); /* Obtain search DN and password used to connect to the server. */ if (!(ldapinfo.searchdn = config_get_string("auth-ldap-searchdn"))) { log_print(LOG_ERR, _("auth_ldap_init: no auth-ldap-searchdn directive in config")); goto fail; } else if (!(ldapinfo.password = config_get_string("auth-ldap-password"))) { log_print(LOG_ERR, _("auth_ldap_init: no auth-ldap-password directive in config; anonymous bind is not permitted")); goto fail; } /* Filter substitution string. */ if ((s = config_get_string("auth-ldap-filter"))) ldapinfo.filter_spec = xstrdup(s); else log_print(LOG_WARNING, _("auth_ldap_init: using default auth-ldap-filter `%s'"), ldapinfo.filter_spec); if ((s = config_get_string("auth-ldap-scope"))) { if (strcasecmp(s, "subtree") == 0) ldapinfo.scope = LDAP_SCOPE_SUBTREE; else if (strcasecmp(s, "base") == 0) ldapinfo.scope = LDAP_SCOPE_BASE; else if (strcasecmp(s, "onelevel") == 0) ldapinfo.scope = LDAP_SCOPE_ONELEVEL; else log_print(LOG_WARNING, _("auth_ldap_init: unknown scope specification `%s'; using default, `subtree'"), s); } /* Mailbox locations, or attribute which specifies it. */ s = config_get_string("auth-ldap-mailbox"); t = config_get_string("auth-ldap-mailbox-attr"); if (!s && t) { ldapinfo.attr.mailbox = xstrdup(t); if ((s = config_get_string("auth-ldap-mboxtype-attr"))) ldapinfo.attr.mboxtype = xstrdup(s); else log_print(LOG_WARNING, _("auth_ldap_init: will guess mailbox types based upon filename"), ldapinfo.attr.mailbox); } else if (s && t) { log_print(LOG_ERR, _("auth_ldap_init: both an auth-ldap-mailbox and an auth-ldap-mailbox-attr directive were specified")); goto fail; } /* The UID and GID used to access the mailbox may be specified in the * configuration file or in the directory. */ s = config_get_string("auth-ldap-mail-user"); t = config_get_string("auth-ldap-mail-user-attr"); if (s && !t) { if (!parse_uid(s, &ldapinfo.uid)) { log_print(LOG_ERR, _("auth_ldap_init: auth-ldap-mail-user directive `%s' does not make sense"), s); goto fail; } } else if (!s && t) ldapinfo.attr.user = xstrdup(t); else if (s && t) { log_print(LOG_ERR, _("auth_ldap_init: both an auth-ldap-mail-user and an auth-ldap-mail-user-attr directive were specified")); goto fail; } else { log_print(LOG_ERR, _("auth_ldap_init: neither an auth-ldap-mail-user nor an auth-ldap-mail-user-attr directive was specified")); goto fail; } s = config_get_string("auth-ldap-mail-group"); t = config_get_string("auth-ldap-mail-group-attr"); if (s && !t) { if (!parse_gid(s, &ldapinfo.gid)) { log_print(LOG_ERR, _("auth_ldap_init: auth-ldap-mail-group directive `%s' does not make sense"), s); goto fail; } } else if (!s && t) ldapinfo.attr.group = xstrdup(t); else if (s && t) { log_print(LOG_ERR, _("auth_ldap_init: both an auth-ldap-mail-group and an auth-ldap-mail-group-attr directive were specified")); goto fail; } else { log_print(LOG_ERR, _("auth_ldap_init: neither an auth-ldap-mail-group nor an auth-ldap-mail-group-attr directive was specified")); goto fail; } /* Do we use TLS to connect to the server? */ if (config_get_bool("auth-ldap-use-tls")) ldapinfo.tls = 1; r = 1; fail: return r; } extern int verbose; /* in main.c */ /* ldap_strerror: * Return the current error string from the LDAP library. */ static char *ldap_strerror(void) { int ld_errno; ldap_get_option(ldapinfo.ldap, LDAP_OPT_ERROR_NUMBER, &ld_errno); return ldap_err2string(ld_errno); } /* try_ldap_connect_bind: * Try to connect to the LDAP server and bind. */ static int try_ldap_connect_bind(const char *who, const char *passwd) { int ret = LDAP_OTHER, i; /* XXX */ for (i = 0; i < 3; ++i) { if (ldapinfo.ldap || auth_ldap_connect()) { ret = ldap_simple_bind_s(ldapinfo.ldap, who, passwd); if (ret == LDAP_SUCCESS) return LDAP_SUCCESS; else { log_print(LOG_ERR, "try_ldap_connect_bind: ldap_simple_bind_s: %s", ldap_err2string(ret)); ldap_unbind(ldapinfo.ldap); ldapinfo.ldap = NULL; } } else ldapinfo.ldap = NULL; } /* OK, didn't succeed. */ return ret; } /* try_ldap_bind: * Try a bind against the LDAP server. */ static int try_ldap_bind(LDAP *ld, const char *who, const char *passwd) { int ret, i; for (i = 0; i < 3; ++i) { ret = ldap_simple_bind_s(ld, who, passwd); if (ret == LDAP_SUCCESS) return LDAP_SUCCESS; } return ret; } /* auth_ldap_new_user_pass: * Attempt to authenticate user against the directory, using a two-step * search/bind process. */ authcontext auth_ldap_new_user_pass(const char *username, const char *local_part, const char *domain, const char *pass, const char *clienthost /* unused */, const char *serverhost /* unused */) { authcontext a = NULL; char *filter = NULL, *base = NULL, *who; LDAPMessage *ldapres = NULL, *user_attr = NULL; char *user_dn = NULL; int nentries, ret; who = username_string(username, local_part, domain); /* Connect to the server. */ if (try_ldap_connect_bind(ldapinfo.searchdn, ldapinfo.password) != LDAP_SUCCESS) { log_print(LOG_ERR, _("auth_ldap_new_user_pass: unable to connect and bind to LDAP server")); goto fail; } /* Obtain search filter. */ if (!(filter = substitute_filter_params(ldapinfo.filter_spec, username, local_part, domain))) goto fail; if (verbose) log_print(LOG_DEBUG, _("auth_ldap_new_user_pass: LDAP search filter: %s"), filter); /* Obtain search base. */ if (!(base = substitute_filter_params(ldapinfo.dn, username, local_part, domain))) goto fail; /* Look for DN of user in the directory. */ if ((ret = ldap_search_s(ldapinfo.ldap, base, LDAP_SCOPE_SUBTREE, filter, NULL, 0, &ldapres)) != LDAP_SUCCESS) { log_print(LOG_ERR, "auth_ldap_new_user_pass: ldap_search_s: %s", ldap_err2string(ret)); goto fail; } /* There must be only one result. */ switch (nentries = ldap_count_entries(ldapinfo.ldap, ldapres)) { case 1: break; default: log_print(LOG_ERR, _("auth_ldap_new_user_pass: search returned %d entries, should be 0 or 1"), nentries); /* fall through */ case 0: goto fail; } if (!(user_attr = ldap_first_entry(ldapinfo.ldap, ldapres))) { log_print(LOG_ERR, "auth_ldap_new_user_pass: ldap_first_entry: %s", ldap_strerror()); goto fail; } /* Get the dn string from the current entry */ if (!(user_dn = ldap_get_dn(ldapinfo.ldap, user_attr))) { log_print(LOG_ERR, "auth_ldap_new_user_pass: ldap_get_dn: %s", ldap_strerror()); goto fail; } /* Now attempt authentication by binding with the user's credentials. */ if ((ret = try_ldap_bind(ldapinfo.ldap, user_dn, pass)) != LDAP_SUCCESS) { /* Bind failed; user has failed to log in. */ if (ret == LDAP_INVALID_CREDENTIALS) log_print(LOG_ERR, _("auth_ldap_new_user_pass: failed login for %s"), who); else log_print(LOG_ERR, "auth_ldap_new_user_pass: try_ldap_bind: %s", ldap_err2string(ret)); goto fail; } else { /* Bind OK; accumulate information about this user and generate an * authcontext. Collect attributes and off we go. */ uid_t uid = -1; gid_t gid = -1; char *mailbox = NULL, *mboxtype = NULL, *user = NULL, *group = NULL; char *attr; BerElement *ber; for (attr = ldap_first_attribute(ldapinfo.ldap, user_attr, &ber); attr; attr = ldap_next_attribute(ldapinfo.ldap, user_attr, ber)) { char **vals; if (!(vals = ldap_get_values(ldapinfo.ldap, user_attr, attr))) { log_print(LOG_WARNING, "auth_ldap_new_user_pass: ldap_get_values(`%s', `%s'): %s", user_attr, attr, ldap_strerror()); continue; } /* XXX case? */ if (ldapinfo.attr.mailbox && strcasecmp(attr, ldapinfo.attr.mailbox) == 0) mailbox = xstrdup(*vals); else if (ldapinfo.attr.mboxtype && strcasecmp(attr, ldapinfo.attr.mboxtype) == 0) mboxtype = xstrdup(*vals); else if (ldapinfo.attr.user && strcasecmp(attr, ldapinfo.attr.user) == 0) user = xstrdup(*vals); else if (ldapinfo.attr.group && strcasecmp(attr, ldapinfo.attr.group) == 0) group = xstrdup(*vals); ldap_value_free(vals); ldap_memfree(attr); } ber_free(ber, 0); /* Check that we've retrieved all the attributes we need. */ #define GOT_ATTR(a) if (ldapinfo.attr.a && !a) { \ log_print(LOG_ERR, _("auth_ldap_new_user_pass: did not find required attribute `%s' for %s"), \ ldapinfo.attr.a, who); \ goto fail; \ } GOT_ATTR(mailbox); GOT_ATTR(mboxtype); GOT_ATTR(user); GOT_ATTR(group); #undef GOT_ATTR /* Test user/group. XXX values specified in LDAP override those in config. */ uid = ldapinfo.uid; gid = ldapinfo.gid; if (user && !parse_uid(user, &uid)) log_print(LOG_ERR, _("auth_ldap_new_user_pass: unix user `%s' for %s does not make sense"), user, who); else if (group && !parse_gid(group, &gid)) log_print(LOG_ERR, _("auth_ldap_new_user_pass: unix group `%s' for %s does not make sense"), group, who); else { struct passwd *pw; char *home = NULL; pw = getpwuid(uid); if (pw) home = pw->pw_dir; /* OK, looks like we can actually do the authentication. */ if (mailbox && !mboxtype) { /* Guess mailbox type based upon name of mailbox. */ if (mailbox[strlen(mailbox) - 1] == '/') a = authcontext_new(uid, gid, "maildir", mailbox, home); else a = authcontext_new(uid, gid, "bsd", mailbox, home); } else if (mailbox) /* Fully specified. */ a = authcontext_new(uid, gid, mboxtype, mailbox, home); else /* Let the mailbox sort itself out.... */ a = authcontext_new(uid, gid, NULL, NULL, home); } xfree(mailbox); xfree(mboxtype); xfree(user); xfree(group); } fail: /* Ugly: force the LDAP library to free user_addr. */ if (user_attr) while (ldap_next_entry(ldapinfo.ldap, ldapres)); if (ldapres) ldap_msgfree(ldapres); if (user_dn) ldap_memfree(user_dn); xfree(filter); xfree(base); /* auth_ldap_close();*/ return a; } /* auth_ldap_close: * Close the ldap connection. */ void auth_ldap_close() { if (ldapinfo.ldap) { ldap_unbind(ldapinfo.ldap); ldapinfo.ldap = NULL; } } /* auth_ldap_postfork: * Post-fork cleanup. */ void auth_ldap_postfork() { ldapinfo.ldap = NULL; /* XXX */ } /* ldap_escape: * Form an escaped version of a string for use in an LDAP filter. */ static char *ldap_escape(const char *s) { static char *t; static size_t tlen; size_t l; char *q; const char *p; if (tlen < (l = strlen(s) * 3 + 1)) { tlen = l; t = xrealloc(t, tlen); } for (p = s, q = t; *p; ++p) if (strchr("*()\\", *p)) { sprintf(q, "\\%02x", (unsigned int)*p); q += 3; } else *q++ = *p; *q = 0; return t; } /* substitute_filter_params: * Given a filter template, local part and domain, construct a real filter * string. */ static char *substitute_filter_params(const char *template, const char *user, const char *local_part, const char *domain) { char *filter = NULL, *u = NULL, *l = NULL, *d = NULL; struct sverr err; u = xstrdup(ldap_escape(user)); if (local_part) l = xstrdup(ldap_escape(local_part)); if (domain) d = xstrdup(ldap_escape(domain)); filter = substitute_variables(template, &err, 3, "user", u, "local_part", l, "domain", d); if (!filter && err.code != sv_nullvalue) log_print(LOG_ERR, _("substitute_filter_params: %s near `%.16s'"), err.msg, template + err.offset); xfree(u); xfree(l); xfree(d); return filter; } #endif /* AUTH_LDAP */