/*
    SSSD

    LDAP Backend Module

    Authors:
        Sumit Bose <sbose@redhat.com>

    Copyright (C) 2008 Red Hat

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#ifdef WITH_MOZLDAP
#define LDAP_OPT_SUCCESS LDAP_SUCCESS
#define LDAP_TAG_EXOP_MODIFY_PASSWD_ID  ((ber_tag_t) 0x80U)
#define LDAP_TAG_EXOP_MODIFY_PASSWD_OLD ((ber_tag_t) 0x81U)
#define LDAP_TAG_EXOP_MODIFY_PASSWD_NEW ((ber_tag_t) 0x82U)
#endif

#include <errno.h>
#include <sys/time.h>

#include <security/pam_modules.h>

#include "util/util.h"
#include "db/sysdb.h"
#include "providers/dp_backend.h"
#include "providers/ldap/sdap_async.h"

struct sdap_auth_ctx {
    struct be_ctx *be;
    struct sdap_options *opts;
};

/* ==Get-User-DN========================================================== */

struct get_user_dn_state {
    struct tevent_context *ev;
    struct sdap_auth_ctx *ctx;
    struct sdap_handle *sh;

    const char **attrs;
    const char *name;

    char *dn;
};

static void get_user_dn_done(void *pvt, int err, struct ldb_result *res);

struct tevent_req *get_user_dn_send(TALLOC_CTX *memctx,
                                    struct tevent_context *ev,
                                    struct sdap_auth_ctx *ctx,
                                    struct sdap_handle *sh,
                                    const char *username)
{
    struct tevent_req *req;
    struct get_user_dn_state *state;
    int ret;

    req = tevent_req_create(memctx, &state, struct get_user_dn_state);
    if (!req) return NULL;

    state->ev = ev;
    state->ctx = ctx;
    state->sh = sh;
    state->name = username;

    state->attrs = talloc_array(state, const char *, 2);
    if (!state->attrs) {
        talloc_zfree(req);
        return NULL;
    }
    state->attrs[0] = SYSDB_ORIG_DN;
    state->attrs[1] = NULL;

    /* this sysdb call uses a sysdn operation, which means it will be
     * schedule only after we return, no timer hack needed */
    ret = sysdb_get_user_attr(state, state->ctx->be->sysdb,
                              state->ctx->be->domain, state->name,
                              state->attrs, get_user_dn_done, req);
    if (ret) {
        tevent_req_error(req, ret);
        tevent_req_post(req, ev);
    }

    return req;
}

static void get_user_dn_done(void *pvt, int err, struct ldb_result *res)
{
    struct tevent_req *req = talloc_get_type(pvt, struct tevent_req);
    struct get_user_dn_state *state = tevent_req_data(req,
                                           struct get_user_dn_state);
    const char *dn;

    if (err != LDB_SUCCESS) {
        tevent_req_error(req, EIO);
        return;
    }

    switch (res->count) {
    case 0:
        /* FIXME: not in cache, needs a true search */
        tevent_req_error(req, ENOENT);
        break;

    case 1:
        dn = ldb_msg_find_attr_as_string(res->msgs[0], SYSDB_ORIG_DN, NULL);
        if (!dn) {
            /* TODO: try to search ldap server ? */

            /* FIXME: remove once we store originalDN on every call
             * NOTE: this is wrong, works only with some DITs */
            dn = talloc_asprintf(state, "%s=%s,%s",
                        state->ctx->opts->user_map[SDAP_AT_USER_NAME].name,
                        state->name,
                        sdap_go_get_string(state->ctx->opts->basic,
                                           SDAP_USER_SEARCH_BASE));
            if (!dn) {
                tevent_req_error(req, ENOMEM);
                break;
            }
        }
        state->dn = talloc_strdup(state, dn);
        if (!state->dn) {
            tevent_req_error(req, ENOMEM);
            break;
        }

        tevent_req_done(req);
        break;

    default:
        DEBUG(1, ("A user search by name (%s) returned > 1 results!\n",
                  state->name));
        tevent_req_error(req, EFAULT);
        break;
    }
}

static int get_user_dn_recv(struct tevent_req *req,
                            TALLOC_CTX *memctx, char **dn)
{
    struct get_user_dn_state *state = tevent_req_data(req,
                                           struct get_user_dn_state);
    enum tevent_req_state tstate;
    uint64_t err;

    if (tevent_req_is_error(req, &tstate, &err)) {
        return err;
    }

    *dn = talloc_steal(memctx, state->dn);
    if (!*dn) return ENOMEM;

    return EOK;
}

/* ==Authenticate-User==================================================== */

struct auth_state {
    struct tevent_context *ev;
    struct sdap_auth_ctx *ctx;
    const char *username;
    struct sdap_blob password;

    struct sdap_handle *sh;

    enum sdap_result result;
    char *dn;
};

static void auth_connect_done(struct tevent_req *subreq);
static void auth_get_user_dn_done(struct tevent_req *subreq);
static void auth_bind_user_done(struct tevent_req *subreq);

static struct tevent_req *auth_send(TALLOC_CTX *memctx,
                                    struct tevent_context *ev,
                                    struct sdap_auth_ctx *ctx,
                                    const char *username,
                                    struct sdap_blob password)
{
    struct tevent_req *req, *subreq;
    struct auth_state *state;

    req = tevent_req_create(memctx, &state, struct auth_state);
    if (!req) return NULL;

    state->ev = ev;
    state->ctx = ctx;
    state->username = username;
    state->password = password;

    subreq = sdap_connect_send(state, ev, ctx->opts, true);
    if (!subreq) goto fail;

    tevent_req_set_callback(subreq, auth_connect_done, req);

    return req;

fail:
    talloc_zfree(req);
    return NULL;
}

static void auth_connect_done(struct tevent_req *subreq)
{
    struct tevent_req *req = tevent_req_callback_data(subreq,
                                                      struct tevent_req);
    struct auth_state *state = tevent_req_data(req,
                                                    struct auth_state);
    int ret;

    ret = sdap_connect_recv(subreq, state, &state->sh);
    talloc_zfree(subreq);
    if (ret) {
        tevent_req_error(req, ret);
        return;
    }

    subreq = get_user_dn_send(state, state->ev,
                              state->ctx, state->sh,
                              state->username);
    if (!subreq) {
        tevent_req_error(req, ENOMEM);
        return;
    }

    tevent_req_set_callback(subreq, auth_get_user_dn_done, req);
}

static void auth_get_user_dn_done(struct tevent_req *subreq)
{
    struct tevent_req *req = tevent_req_callback_data(subreq,
                                                      struct tevent_req);
    struct auth_state *state = tevent_req_data(req,
                                                    struct auth_state);
    int ret;

    ret = get_user_dn_recv(subreq, state, &state->dn);
    talloc_zfree(subreq);
    if (ret) {
        tevent_req_error(req, ret);
        return;
    }

    subreq = sdap_auth_send(state, state->ev, state->sh,
                            state->dn, "password", state->password);
    if (!subreq) {
        tevent_req_error(req, ENOMEM);
        return;
    }

    tevent_req_set_callback(subreq, auth_bind_user_done, req);
}

static void auth_bind_user_done(struct tevent_req *subreq)
{
    struct tevent_req *req = tevent_req_callback_data(subreq,
                                                      struct tevent_req);
    struct auth_state *state = tevent_req_data(req,
                                                    struct auth_state);
    int ret;

    ret = sdap_auth_recv(subreq, &state->result);
    talloc_zfree(subreq);
    if (ret) {
        tevent_req_error(req, ret);
        return;
    }

    tevent_req_done(req);
}

int auth_recv(struct tevent_req *req, enum sdap_result *result,
                     TALLOC_CTX *memctx, struct sdap_handle **sh, char **dn)
{
    struct auth_state *state = tevent_req_data(req,
                                                    struct auth_state);
    enum tevent_req_state tstate;
    uint64_t err;

    if (tevent_req_is_error(req, &tstate, &err)) {
        if (err == ETIMEDOUT) *result = SDAP_UNAVAIL;
        else *result = SDAP_ERROR;
        return EOK;
    }

    if (sh != NULL) {
        *sh = talloc_steal(memctx, state->sh);
        if (*sh == NULL) return ENOMEM;
    }

    if (dn != NULL) {
        *dn = talloc_steal(memctx, state->dn);
        if (*dn == NULL) return ENOMEM;
    }

    *result = state->result;
    return EOK;
}

/* ==Perform-Password-Change===================== */

struct sdap_pam_chpass_state {
    struct be_req *breq;
    struct pam_data *pd;
    const char *username;
    char *dn;
    char *password;
    char *new_password;
    struct sdap_handle *sh;
};

static void sdap_auth4chpass_done(struct tevent_req *req);
static void sdap_pam_chpass_done(struct tevent_req *req);
static void sdap_pam_auth_reply(struct be_req *breq, int result);

static void sdap_pam_chpass_send(struct be_req *breq)
{
    struct sdap_pam_chpass_state *state;
    struct sdap_auth_ctx *ctx;
    struct tevent_req *subreq;
    struct pam_data *pd;
    struct sdap_blob authtok;

    ctx = talloc_get_type(breq->be_ctx->bet_info[BET_CHPASS].pvt_bet_data,
                          struct sdap_auth_ctx);
    pd = talloc_get_type(breq->req_data, struct pam_data);

    if (be_is_offline(ctx->be)) {
        DEBUG(4, ("Backend is marked offline, retry later!\n"));
        pd->pam_status = PAM_AUTHINFO_UNAVAIL;
        goto done;
    }

    DEBUG(2, ("starting password change request for user [%s].\n", pd->user));

    pd->pam_status = PAM_SYSTEM_ERR;

    if (pd->cmd != SSS_PAM_CHAUTHTOK) {
        DEBUG(2, ("chpass target was called by wrong pam command.\n"));
        goto done;
    }

    state = talloc_zero(breq, struct sdap_pam_chpass_state);
    if (!state) goto done;

    state->breq = breq;
    state->pd = pd;
    state->username = pd->user;
    state->password = talloc_strndup(state,
                                     (char *)pd->authtok, pd->authtok_size);
    if (!state->password) goto done;
    talloc_set_destructor((TALLOC_CTX *)state->password,
                          password_destructor);
    state->new_password = talloc_strndup(state,
                                         (char *)pd->newauthtok,
                                         pd->newauthtok_size);
    if (!state->new_password) goto done;
    talloc_set_destructor((TALLOC_CTX *)state->new_password,
                          password_destructor);

    authtok.data = (uint8_t *)state->password;
    authtok.length = strlen(state->password);
    subreq = auth_send(breq, breq->be_ctx->ev,
                       ctx, state->username, authtok);
    if (!subreq) goto done;

    tevent_req_set_callback(subreq, sdap_auth4chpass_done, state);
    return;
done:
    sdap_pam_auth_reply(breq, pd->pam_status);
}

static void sdap_auth4chpass_done(struct tevent_req *req)
{
    struct sdap_pam_chpass_state *state =
                    tevent_req_callback_data(req, struct sdap_pam_chpass_state);
    struct tevent_req *subreq;
    enum sdap_result result;
    int ret;

    ret = auth_recv(req, &result, state, &state->sh, &state->dn);
    talloc_zfree(req);
    if (ret) {
        state->pd->pam_status = PAM_SYSTEM_ERR;
        goto done;
    }


    switch (result) {
    case SDAP_AUTH_SUCCESS:
        DEBUG(7, ("user [%s] successfully authenticated.\n", state->dn));
        subreq = sdap_exop_modify_passwd_send(state,
                                              state->breq->be_ctx->ev,
                                              state->sh,
                                              state->dn,
                                              state->password,
                                              state->new_password);

        if (!subreq) {
            DEBUG(2, ("Failed to change password for %s\n", state->username));
            goto done;
        }

        tevent_req_set_callback(subreq, sdap_pam_chpass_done, state);
        return;
        break;
    default:
        state->pd->pam_status = PAM_SYSTEM_ERR;
    }

done:
    sdap_pam_auth_reply(state->breq, state->pd->pam_status);
}

static void sdap_pam_chpass_done(struct tevent_req *req)
{
    struct sdap_pam_chpass_state *state =
                    tevent_req_callback_data(req, struct sdap_pam_chpass_state);
    enum sdap_result result;
    int ret;

    ret = sdap_exop_modify_passwd_recv(req, &result);
    talloc_zfree(req);
    if (ret) {
        state->pd->pam_status = PAM_SYSTEM_ERR;
        goto done;
    }

    switch (result) {
    case SDAP_SUCCESS:
        state->pd->pam_status = PAM_SUCCESS;
        break;
    default:
        state->pd->pam_status = PAM_SYSTEM_ERR;
    }

done:
    sdap_pam_auth_reply(state->breq, state->pd->pam_status);
}
/* ==Perform-User-Authentication-and-Password-Caching===================== */

struct sdap_pam_auth_state {
    struct be_req *breq;
    struct pam_data *pd;
    const char *username;
    struct sdap_blob password;
};

static void sdap_pam_auth_done(struct tevent_req *req);
static void sdap_password_cache_done(struct tevent_req *req);
static void sdap_pam_auth_reply(struct be_req *breq, int result);

/* FIXME: convert caller to tevent_req too ?*/
static void sdap_pam_auth_send(struct be_req *breq)
{
    struct sdap_pam_auth_state *state;
    struct sdap_auth_ctx *ctx;
    struct tevent_req *subreq;
    struct pam_data *pd;

    ctx = talloc_get_type(breq->be_ctx->bet_info[BET_AUTH].pvt_bet_data, struct sdap_auth_ctx);
    pd = talloc_get_type(breq->req_data, struct pam_data);

    if (be_is_offline(ctx->be)) {
        DEBUG(4, ("Backend is marked offline, retry later!\n"));
        pd->pam_status = PAM_AUTHINFO_UNAVAIL;
        goto done;
    }

    pd->pam_status = PAM_SYSTEM_ERR;

    switch (pd->cmd) {
    case SSS_PAM_AUTHENTICATE:

        state = talloc_zero(breq, struct sdap_pam_auth_state);
        if (!state) goto done;

        state->breq = breq;
        state->pd = pd;
        state->username = pd->user;
        state->password.data = pd->authtok;
        state->password.length = pd->authtok_size;

        subreq = auth_send(breq, breq->be_ctx->ev, ctx,
                           state->username, state->password);
        if (!subreq) goto done;

        tevent_req_set_callback(subreq, sdap_pam_auth_done, state);
        return;

/* FIXME: handle other cases */
    case SSS_PAM_CHAUTHTOK:
        break;

    default:
        pd->pam_status = PAM_SUCCESS;
    }

done:
    sdap_pam_auth_reply(breq, pd->pam_status);
}

static void sdap_pam_auth_done(struct tevent_req *req)
{
    struct sdap_pam_auth_state *state =
                    tevent_req_callback_data(req, struct sdap_pam_auth_state);
    struct tevent_req *subreq;
    enum sdap_result result;
    int ret;

    ret = auth_recv(req, &result, NULL, NULL, NULL);
    talloc_zfree(req);
    if (ret) {
        state->pd->pam_status = PAM_SYSTEM_ERR;
        goto done;
    }

    switch (result) {
    case SDAP_AUTH_SUCCESS:
        state->pd->pam_status = PAM_SUCCESS;
        break;
    case SDAP_AUTH_FAILED:
        state->pd->pam_status = PAM_CRED_INSUFFICIENT;
        break;
    case SDAP_UNAVAIL:
        state->pd->pam_status = PAM_AUTHINFO_UNAVAIL;
        break;
    default:
        state->pd->pam_status = PAM_SYSTEM_ERR;
    }

    if (result == SDAP_UNAVAIL) {
        be_mark_offline(state->breq->be_ctx);
        goto done;
    }

    if (result == SDAP_AUTH_SUCCESS &&
        state->breq->be_ctx->domain->cache_credentials) {

        char *password = talloc_strndup(state, (char *)
                                        state->password.data,
                                        state->password.length);
        if (!password) {
            DEBUG(2, ("Failed to cache password for %s\n", state->username));
            goto done;
        }
        talloc_set_destructor((TALLOC_CTX *)password, password_destructor);

        subreq = sysdb_cache_password_send(state,
                                           state->breq->be_ctx->ev,
                                           state->breq->be_ctx->sysdb,
                                           NULL,
                                           state->breq->be_ctx->domain,
                                           state->username, password);

        /* password caching failures are not fatal errors */
        if (!subreq) {
            DEBUG(2, ("Failed to cache password for %s\n", state->username));
            goto done;
        }

        tevent_req_set_callback(subreq, sdap_password_cache_done, state);
        return;
    }

done:
    sdap_pam_auth_reply(state->breq, state->pd->pam_status);
}

static void sdap_password_cache_done(struct tevent_req *subreq)
{
    struct sdap_pam_auth_state *state = tevent_req_callback_data(subreq,
                                                struct sdap_pam_auth_state);
    int ret;

    ret = sysdb_cache_password_recv(subreq);
    talloc_zfree(subreq);
    if (ret) {
        /* password caching failures are not fatal errors */
        DEBUG(2, ("Failed to cache password for %s\n", state->username));
    } else {
        DEBUG(4, ("Password successfully cached for %s\n", state->username));
    }

    sdap_pam_auth_reply(state->breq, state->pd->pam_status);
}

static void sdap_pam_auth_reply(struct be_req *req, int result)
{
    const char *errstr = NULL;
    if (result) errstr = "Operation failed";
    req->fn(req, result, errstr);
}

/* ==Module-Initialization-and-Dispose==================================== */

static void sdap_shutdown(struct be_req *req)
{
    /* TODO: Clean up any internal data */
    req->fn(req, EOK, NULL);
}

struct bet_ops sdap_auth_ops = {
    .handler = sdap_pam_auth_send,
    .finalize = sdap_shutdown
};

struct bet_ops sdap_chpass_ops = {
    .handler = sdap_pam_chpass_send,
    .finalize = sdap_shutdown
};

int sssm_ldap_auth_init(struct be_ctx *bectx,
                        struct bet_ops **ops,
                        void **pvt_data)
{
    int ldap_opt_x_tls_require_cert;
    struct sdap_auth_ctx *ctx;
    char *tls_reqcert;
    int ret;

    ctx = talloc(bectx, struct sdap_auth_ctx);
    if (!ctx) return ENOMEM;

    ctx->be = bectx;

    ret = sdap_get_options(ctx, bectx->cdb, bectx->conf_path,
                              &ctx->opts);
    if (ret != EOK) goto done;

    tls_reqcert = sdap_go_get_string(ctx->opts->basic, SDAP_TLS_REQCERT);
    if (tls_reqcert) {
        if (strcasecmp(tls_reqcert, "never") == 0) {
            ldap_opt_x_tls_require_cert = LDAP_OPT_X_TLS_NEVER;
        }
        else if (strcasecmp(tls_reqcert, "allow") == 0) {
            ldap_opt_x_tls_require_cert = LDAP_OPT_X_TLS_ALLOW;
        }
        else if (strcasecmp(tls_reqcert, "try") == 0) {
            ldap_opt_x_tls_require_cert = LDAP_OPT_X_TLS_TRY;
        }
        else if (strcasecmp(tls_reqcert, "demand") == 0) {
            ldap_opt_x_tls_require_cert = LDAP_OPT_X_TLS_DEMAND;
        }
        else if (strcasecmp(tls_reqcert, "hard") == 0) {
            ldap_opt_x_tls_require_cert = LDAP_OPT_X_TLS_HARD;
        }
        else {
            DEBUG(1, ("Unknown value for tls_reqcert.\n"));
            ret = EINVAL;
            goto done;
        }
        /* LDAP_OPT_X_TLS_REQUIRE_CERT has to be set as a global option,
         * because the SSL/TLS context is initialized from this value. */
        ret = ldap_set_option(NULL, LDAP_OPT_X_TLS_REQUIRE_CERT,
                              &ldap_opt_x_tls_require_cert);
        if (ret != LDAP_OPT_SUCCESS) {
            DEBUG(1, ("ldap_set_option failed: %s\n", ldap_err2string(ret)));
            ret = EIO;
            goto done;
        }
    }

    *ops = &sdap_auth_ops;
    *pvt_data = ctx;
    ret = EOK;

done:
    if (ret != EOK) {
        talloc_free(ctx);
    }
    return ret;
}

int sssm_ldap_chpass_init(struct be_ctx *bectx,
                          struct bet_ops **ops,
                          void **pvt_data)
{
    int ret;
    ret = sssm_ldap_auth_init(bectx, ops, pvt_data);
    *ops = &sdap_chpass_ops;
    return ret;
}