/*
 * 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 <sys/types.h> /* BSD needs this here, apparently. */

#include <lber.h>
#include <ldap.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

#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 */


syntax highlighted by Code2HTML, v. 0.9.1