/*
 * auth_pgsql.c:
 * Authenticate users against a Postgres database.
 *
 * XXX should have the connection retrying behaviour of auth-mysql.
 *
 * Copyright (c) 2003 Chris Lightfoot, Stephen White. All rights reserved.
 *
 */

#ifdef HAVE_CONFIG_H
#include "configuration.h"
#endif /* HAVE_CONFIG_H */

#ifdef AUTH_PGSQL
static const char rcsid[] = "$Id: auth_pgsql.c,v 1.1 2003/07/14 23:31:20 chris Exp $";

#include <sys/types.h> /* BSD needs this here, apparently. */

#ifdef HAVE_CRYPT_H
#include <crypt.h>
#endif

#include <grp.h>
#include <pwd.h>
#include <libpq-fe.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

#include "auth_pgsql.h"
#include "authswitch.h"
#include "config.h"
#include "password.h"
#include "stringmap.h"
#include "util.h"

/* 
 * Old libpq doesn't have this function....
 */
#ifndef SYSTEM_PQESCAPESTRING

/* PQescapeStringLocal TO FROM LENGTH
 * Local copy of the escape-string function which escapes dangerous characters
 * in query FROM, writing the modified string into TO. */
size_t PQescapeStringLocal(char *to, const char *from, size_t length) {
    const char *source;
    char *target;
    unsigned int remaining;

    source = from;
    target = to;
    remaining = length;

    while (remaining > 0) {
        switch (*source) {
            case '\\':
                *target = '\\';
                target++;
                *target = '\\';
                break;

            case '\'':
                *target = '\'';
                target++;
                *target = '\'';
                break;

            case '"':
                *target = '\\';
                target++;
                *target = '"';
                break;

            case '\0':
                *target = '\\';
                target++;
                *target = '0';
                break;

            default:
                *target = *source;
                break;
        }
        source++;
        target++;
        remaining--;
    }

    /* Write the terminating NUL character. */
    *target = '\0';

    return target - to;
}

#define PQescapeString(to, from, len) PQescapeStringLocal(to, from, len)

#endif /* SYSTEM_PQESCAPESTRING */

/* Default query templates. The returned fields are:
 *  [0] location of mailbox
 *  [1] password hash
 *  [2] unix user
 *  [3] mailbox type
 */
char *user_pass_query_template =
    "SELECT domain.path || '/'  || popbox.mbox_name, popbox.password_hash, "
            "domain.unix_user, 'bsd' "
      "FROM popbox, domain "
     "WHERE popbox.local_part = '$(local_part)' "
       "AND popbox.domain_name = '$(domain)' "
       "AND popbox.domain_name = domain.domain_name";
       
char *apop_query_template =
    "SELECT domain.path || '/' || popbox.mbox_name, popbox.password_hash, "
            "domain.unix_user, 'bsd' "
      "FROM popbox, domain "
     "WHERE popbox.local_part = '$(local_part)' "
       "AND popbox.domain_name = '$(domain)' "
       "AND popbox.domain_name = domain.domain_name";

char *onlogin_query_template = NULL;

/* GID used to access mail spool (if any). */
int use_gid;
gid_t mail_gid;

static char *substitute_query_params(const char *temp, const char *user, const char *local_part, const char *domain, const char *clienthost, const char *serverhost);

/* pg_strerror CONNECTION
 * Wrapper for PQerrorMessage which removes any trailing newline (aargh). */
static char *pg_strerror(const PGconn *conn) {
    static char *s;
    s = PQerrorMessage(conn);
    s[strlen(s) - 1] = 0;   /* ugh */
    return s;
}

/* strclr STRING
 * Clear the contents of STRING. */
static void strclr(char *s) {
    memset(s, 0, strlen(s));
}

/* auth_pgsql_init
 * Initialise the database connection driver. Clears the config directives
 * associated with the database so that a user cannot recover them with a
 * debugger. */
PGconn *pg_conn;

int auth_pgsql_init(void) {
    char *username = NULL, *password = NULL, *hostname = NULL, *database = NULL, *localhost = "localhost", *s;
    char *dbconnect = NULL;
    int ret = 0;

    if ((s = config_get_string("auth-pgsql-username")))
        username = s;
    else {
        log_print(LOG_ERR, _("auth_pgsql_init: no auth-pgsql-username directive in config"));
        goto fail;
    }

    if ((s = config_get_string("auth-pgsql-password")))
        password = s;
    else {
        log_print(LOG_WARNING, _("auth_pgsql_init: no auth-pgsql-password directive in config; using blank password"));
        password = "";
    }

    if ((s = config_get_string("auth-pgsql-database")))
        database = s;
    else {
        log_print(LOG_ERR, _("auth_pgsql_init: no auth-pgsql-database directive in config"));
        goto fail;
    }

    if ((s = config_get_string("auth-pgsql-hostname")))
        hostname = s;
    else hostname = localhost;

    /* Obtain query templates. The special string `none' means `don't use
     * any query for this action'. */
    if ((s = config_get_string("auth-pgsql-pass-query")))
        user_pass_query_template = s;
    if (strcmp(user_pass_query_template, "none") == 0)
        user_pass_query_template = NULL;
    
    if ((s = config_get_string("auth-pgsql-apop-query")))
        apop_query_template = s;
    if (strcmp(apop_query_template, "none") == 0)
        apop_query_template = NULL;

    /* This is an optional action to put a row into the database after a
     * successful login, for POP-before-SMTP relaying. */
    if ((s = config_get_string("auth-pgsql-onlogin-query")))
        onlogin_query_template = s;

    /* Obtain gid to use */
    if ((s = config_get_string("auth-pgsql-mail-group"))) {
        if (!parse_gid(s, &mail_gid)) {
            log_print(LOG_ERR, _("auth_pgsql_init: auth-pgsql-mail-group directive `%s' does not make sense"), s);
            goto fail;
        }
        use_gid = 1;
    }

    /* What a horrid interface.... */
    dbconnect = xmalloc(sizeof("dbname= host= user= password=") + strlen(database) + strlen(hostname) + strlen(username) + strlen(password));
    
    sprintf(dbconnect, "dbname=%s%s%s%s%s%s%s",
            database,
            hostname ? " host=" : "",
            hostname ? hostname : "",
            username ? " user=" : "",
            username ? username : "",
            password ? " password=" : "",
            password ? password : ""
        );
    
    pg_conn = PQconnectdb(dbconnect);
    if (PQstatus(pg_conn) == CONNECTION_BAD) {
        log_print(LOG_ERR, "auth_pgsql_init: PQconnectdb: failed (%s)", PQerrorMessage(pg_conn));
        goto fail;
    }

    ret = 1;

fail:
    if (username) strclr(username);
    if (password) strclr(password);
    if (hostname && hostname != localhost) strclr(hostname);
    if (database) strclr(database);
    xfree(dbconnect);

    return ret;
}

extern int verbose; /* in main.c */

/* auth_pgsql_new_apop NAME LOCAL-PART DOMAIN TIMESTAMP DIGEST CLIENTHOST SERVERHOST
 * Attempt to authenticate a user via APOP, using the template SELECT query in
 * the config file or the default defined above otherwise. */
authcontext auth_pgsql_new_apop(const char *name, const char *local_part, const char *domain, const char *timestamp, const unsigned char *digest, const char *clienthost /* unused */, const char *serverhost) {
    char *query = NULL;
    authcontext a = NULL;
    char *who;
    PGresult *res = NULL;

    who = username_string(name, local_part, domain);

    if (!pg_conn || !apop_query_template) return NULL;

    /* Obtain the actual query to use. */
    if (!(query = substitute_query_params(apop_query_template, name, local_part, domain, NULL, serverhost)))
        goto fail;

    if (verbose)
        log_print(LOG_DEBUG, "auth_pgsql_new_apop: SQL query: %s", query);

    if ((res = PQexec(pg_conn, query))) {
        int i;

        if (PQresultStatus(res) != PGRES_TUPLES_OK) {
            log_print(LOG_ERR, "auth_pgsql_new_apop: error executing query: %s", PQresultErrorMessage(res));
            goto fail;
        }

        if (PQnfields(res) != 4) {
            log_print(LOG_ERR, "auth_pgsql_new_apop: %d fields returned by query, should be 4: mailbox location, password hash, unix user, mailbox type", PQnfields(res));
            goto fail;
        }

        switch (i = PQntuples(res)) {
        case 0:
            break;
        case 1: {
                uid_t uid;
                struct passwd *pw;
                char *user, *passwd, *mailbox, *mboxdrv;

                /* Check that user has UID and password. */
                if (PQgetisnull(res, 0, 2)) {
                    log_print(LOG_ERR, _("auth_pgsql_new_apop: UID for user %s is NULL"), who);
                    goto fail;
                } else if (PQgetisnull(res, 0, 1)) {
                    log_print(LOG_ERR, _("auth_pgsql_new_apop: password hash for user %s is NULL"), who);
                    goto fail;
                }

                /* Get the various fields. */
                passwd = PQgetvalue(res, 0, 1);
                user = PQgetvalue(res, 0, 2);
                
                mailbox = PQgetisnull(res, 0, 0) ? NULL : PQgetvalue(res, 0, 0);
                mboxdrv = PQgetisnull(res, 0, 3) ? NULL : PQgetvalue(res, 0, 3);
                    
                if (!check_password_apop(who, passwd, timestamp, digest)) {
                    log_print(LOG_WARNING, _("auth_pgsql_new_apop: failed login for %s"), who);
                    goto fail;
                }

                /* User was not lying (about her password) */
                if (!parse_uid(user, &uid)) {
                    log_print(LOG_ERR, _("auth_pgsql_new_apop: unix user `%s' for %s does not make sense"), user, who);
                    break;
                }

                pw = getpwuid(uid);

                if (!pw) {
                    log_print(LOG_ERR, "auth_pgsql_apop: getpwuid(%d): %m", (int)uid);
                    break;
                }

                a = authcontext_new(pw->pw_uid, use_gid ? mail_gid : pw->pw_gid, mboxdrv, mailbox, pw->pw_dir);

                break;
            }

        default:
            log_print(LOG_ERR, _("auth_pgsql_new_apop: database inconsistency: query for %s returned %d rows"), name, i);
            break;
        }

        PQclear(res);
    } else
        log_print(LOG_ERR, _("auth_pgsql_new_apop: PQexec: %s"), pg_strerror(pg_conn));

fail:
    xfree(query);
    
    return a;
}

/* auth_pgsql_new_user_pass USER LOCAL-PART DOMAIN PASSWORD CLIENTHOST SERVERHOST
 * Attempt to authenticate a user via USER/PASS, using the template SELECT
 * query in the config file or the default defined above otherwise. */
authcontext auth_pgsql_new_user_pass(const char *user, const char *local_part, const char *domain, const char *pass, const char *clienthost /* unused */, const char *serverhost) {
    char *query = NULL, *who;
    authcontext a = NULL;
    PGresult *res = NULL;

    who = username_string(user, local_part, domain);
    
    if (!pg_conn || !user_pass_query_template) return NULL;

    /* Obtain the actual query to use. */
    if (!(query = substitute_query_params(user_pass_query_template, user, local_part, domain, NULL, serverhost)))
        goto fail;

    if (verbose)
        log_print(LOG_DEBUG, "auth_pgsql_new_user_pass: SQL query: %s", query);

    if ((res = PQexec(pg_conn, query))) {
        int i;

        if (PQresultStatus(res) != PGRES_TUPLES_OK) {
            log_print(LOG_ERR, "auth_pgsql_new_user_pass: error executing query: %s", PQresultErrorMessage(res));
            goto fail;
        }

        if (PQnfields(res) != 4) {
            log_print(LOG_ERR, "auth_pgsql_new_user_pass: %d fields returned by query, should be 4: mailbox location, password hash, unix user, mailbox type", PQnfields(res));
            goto fail;
        }

        switch (i = PQntuples(res)) {
        case 0:
            break;
        case 1: {
                struct passwd *pw;
                uid_t uid;
                char *user, *pwhash, *mailbox, *mboxdrv;

                /* Check that user has UID and password. */
                if (PQgetisnull(res, 0, 2)) {
                    log_print(LOG_ERR, _("auth_pgsql_new_apop: UID for user %s is NULL"), who);
                    goto fail;
                } else if (PQgetisnull(res, 0, 1)) {
                    log_print(LOG_ERR, _("auth_pgsql_new_apop: password hash for user %s is NULL"), who);
                    goto fail;
                }

                /* Get the various fields. */
                pwhash = PQgetvalue(res, 0, 1);
                user = PQgetvalue(res, 0, 2);
                
                mailbox = PQgetisnull(res, 0, 0) ? NULL : PQgetvalue(res, 0, 0);
                mboxdrv = PQgetisnull(res, 0, 3) ? NULL : PQgetvalue(res, 0, 3);

                if (!check_password(who, pwhash, pass, "{md5}")) {
                    log_print(LOG_ERR, _("auth_pgsql_new_user_pass: %s failed login with wrong password"), who);
                    break;
                }

                if (!parse_uid(user, &uid)) {
                    log_print(LOG_ERR, _("auth_pgsql_new_user_pass: unix user `%s' for %s does not make sense"), user, who);
                    break;
                }

                pw = getpwuid(uid);

                if (!pw) {
                    log_print(LOG_ERR, "auth_pgsql_new_user_pass: getpwuid(%d): %m", (int)uid);
                    break;
                }

                a = authcontext_new(pw->pw_uid, use_gid ? mail_gid : pw->pw_gid, mboxdrv, mailbox, pw->pw_dir);
                break;
            }

        default:
            log_print(LOG_ERR, _("auth_pgsql_new_user_pass: database inconsistency: query for %s returned %d rows"), who, i);
            break;
        }

        PQclear(res);
    } else
        log_print(LOG_ERR, _("auth_pgsql_new_apop: PQexec: %s"), pg_strerror(pg_conn));

fail:
    xfree(query);

    return a;
}

/* auth_pgsql_onlogin AUTHCONTEXT CLIENTHOST SERVERHOST
 * If specified, perform a query (action) after a successful login. The
 * variables substituted in the template are $(local_part), $(domain) and
 * $(clienthost), the username, domain, and connecting client host. */
void auth_pgsql_onlogin(const authcontext A, const char *clienthost, const char *serverhost) {
    char *query;
    PGresult *res = NULL;

    if (!pg_conn || !onlogin_query_template) return;

    query = substitute_query_params(onlogin_query_template, A->user, A->local_part, A->domain, clienthost, serverhost);
    if (!query)
        return;

    if (verbose)
        log_print(LOG_DEBUG, _("auth_pgsql_onlogin: SQL query: %s"), query);

    if ((res = PQexec(pg_conn, query))) { /* XXX transactions? */
        if (PQresultStatus(res) != PGRES_TUPLES_OK) {
            log_print(LOG_ERR, "auth_pgsql_onlogin: bad status after PQexec: %s", pg_strerror(pg_conn));
        } else if (PQntuples(res)) {
          /* It's possible that the user put a query in which returned some rows
           * This is bogus but there's not a lot we can do */
            log_print(LOG_WARNING, _("auth_pgsql_onlogin: supplied SQL query returned %d rows, which is dubious"), PQntuples(res));
        }
        PQclear(res);
    } else
        log_print(LOG_ERR, _("auth_pgsql_onlogin: PQexec: %s"), pg_strerror(pg_conn));

    xfree(query);
}

/* auth_pgsql_postfork
 * Post-fork cleanup. */
void auth_pgsql_postfork(void) {
    pg_conn = NULL;
}

/* auth_pgsql_close
 * Close the database connection. */
void auth_pgsql_close(void) {
    if (pg_conn) PQfinish(pg_conn);
}

/* substitute_query_params TEMPLATE USER LOCAL-PART DOMAIN CLIENTHOST SERVERHOST
 * Given a query template, a localpart and a domain, return a copy of the
 * template with the fields filled in. */
static char *substitute_query_params(const char *template, const char *user, const char *local_part, const char *domain, const char *clienthost, const char *serverhost) {
    char *query, *u = NULL, *l = NULL, *d = NULL, *c = NULL, *s = NULL;
    struct sverr err;

    /* Form escaped copies of the user and domain. 
     * The terminating null is added but not counted in the to_length */
    u = xmalloc(strlen(user) * 2 + 1);
    PQescapeString(u,user,strlen(user));
    
    if (local_part) {
        l = xmalloc(strlen(local_part) * 2 + 1);
        PQescapeString(l, local_part, strlen(local_part));
    }

    if (domain) {
        d = xmalloc(strlen(domain) * 2 + 1);
        PQescapeString(d, domain, strlen(domain));
    }

    if (clienthost) {
        c = xmalloc(strlen(clienthost) * 2 + 1);
        PQescapeString(c, clienthost, strlen(clienthost));
    }

    if (serverhost) {
        s = xmalloc(strlen(serverhost) * 2 + 1);
        PQescapeString(s, serverhost, strlen(serverhost));
    }

    /* Do the substitution. */
    query = substitute_variables(template, &err, 5, "user", u, "local_part", l, "domain", d, "clienthost", c, "serverhost", s);

    if (!query && err.code != sv_nullvalue)
        log_print(LOG_ERR, _("substitute_query_params: %s near `%.16s'"), err.msg, template + err.offset);

    xfree(s);
    xfree(c);
    xfree(u);
    xfree(l);
    xfree(d);
    return query;
}

#endif /* AUTH_PGSQL */


syntax highlighted by Code2HTML, v. 0.9.1