/*
    SSSD

    Kerberos 5 Backend Module -- Request a TGT when the system gets online

    Authors:
        Sumit Bose <sbose@redhat.com>

    Copyright (C) 2010 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/>.
*/

#include <security/pam_modules.h>
#ifdef USE_KEYRING
#include <sys/types.h>
#include <keyutils.h>
#endif

#include "providers/krb5/krb5_auth.h"
#include "dhash.h"
#include "util/util.h"
#include "util/find_uid.h"

#define INITIAL_USER_TABLE_SIZE 10

struct deferred_auth_ctx {
    hash_table_t *user_table;
    struct be_ctx *be_ctx;
    struct tevent_context *ev;
    struct krb5_ctx *krb5_ctx;
};

struct auth_data {
    struct be_ctx *be_ctx;
    struct krb5_ctx *krb5_ctx;
    struct pam_data *pd;
};

static void *hash_talloc(const size_t size, void *pvt)
{
    return talloc_size(pvt, size);
}

static void hash_talloc_free(void *ptr, void *pvt)
{
    talloc_free(ptr);
}

static void authenticate_user_done(struct tevent_req *req);
static void authenticate_user(struct tevent_context *ev,
                              struct tevent_timer *te,
                              struct timeval current_time,
                              void *private_data)
{
    struct auth_data *auth_data = talloc_get_type(private_data,
                                                  struct auth_data);
    struct pam_data *pd = auth_data->pd;
    struct tevent_req *req;

    DEBUG_PAM_DATA(9, pd);

    if (pd->authtok == NULL || pd->authtok_size == 0) {
        DEBUG(1, ("Missing authtok for user [%s].\n", pd->user));
        return;
    }

#ifdef USE_KEYRING
    long keysize;
    long keyrevoke;
    int ret;
    keysize = keyctl_read(pd->key_serial, (char *) pd->authtok,
                          pd->authtok_size);
    keyrevoke = keyctl_revoke(pd->key_serial);
    if (keysize == -1) {
        ret = errno;
        DEBUG(1, ("keyctl_read failed [%d][%s].\n", ret, strerror(ret)));
        return;
    } else if (keysize != pd->authtok_size) {
        DEBUG(1, ("keyctl_read returned key with wrong size, "
                  "expect [%d] got [%d].\n", pd->authtok_size, keysize));
        return;
    }
    if (keyrevoke == -1) {
        ret = errno;
        DEBUG(1, ("keyctl_revoke failed [%d][%s].\n", ret, strerror(ret)));
    }
#endif

    req = krb5_auth_send(auth_data, ev, auth_data->be_ctx, auth_data->pd,
                         auth_data->krb5_ctx);
    if (req == NULL) {
        DEBUG(1, ("krb5_auth_send failed.\n"));
        talloc_free(auth_data);
        return;
    }

    tevent_req_set_callback(req, authenticate_user_done, auth_data);
}

static void authenticate_user_done(struct tevent_req *req) {
    struct auth_data *auth_data = tevent_req_callback_data(req,
                                                           struct auth_data);
    int ret;
    int pam_status = PAM_SYSTEM_ERR;
    int dp_err;

    ret = krb5_auth_recv(req, &pam_status, &dp_err);
    talloc_free(req);
    if (ret) {
        DEBUG(1, ("krb5_auth request failed.\n"));
    } else {
        if (pam_status == PAM_SUCCESS) {
            DEBUG(4, ("Successfully authenticated user [%s].\n",
                      auth_data->pd->user));
        } else {
            DEBUG(1, ("Failed to authenticate user [%s].\n",
                      auth_data->pd->user));
        }
    }

    talloc_free(auth_data);
}

static errno_t authenticate_stored_users(
            struct deferred_auth_ctx *deferred_auth_ctx)
{
    int ret;
    hash_table_t *uid_table;
    struct hash_iter_context_t *iter;
    hash_entry_t *entry;
    hash_key_t key;
    hash_value_t value;
    struct pam_data *pd;
    struct auth_data *auth_data;
    struct tevent_timer *te;

    ret = get_uid_table(deferred_auth_ctx, &uid_table);
    if (ret != HASH_SUCCESS) {
        DEBUG(1, ("get_uid_table failed.\n"));
        return ret;
    }

    iter = new_hash_iter_context(deferred_auth_ctx->user_table);
    if (iter == NULL) {
        DEBUG(1, ("new_hash_iter_context failed.\n"));
        return EINVAL;
    }

    while ((entry = iter->next(iter)) != NULL) {
        key.type = HASH_KEY_ULONG;
        key.ul = entry->key.ul;
        pd = talloc_get_type(entry->value.ptr, struct pam_data);

        ret = hash_lookup(uid_table, &key, &value);

        if (ret == HASH_SUCCESS) {
            DEBUG(1, ("User [%s] is still logged in, "
                      "trying online authentication.\n", pd->user));

            auth_data = talloc_zero(deferred_auth_ctx->be_ctx,
                                    struct auth_data);
            if (auth_data == NULL) {
                DEBUG(1, ("talloc_zero failed.\n"));
            } else {
                auth_data->pd = talloc_steal(auth_data, pd);
                auth_data->krb5_ctx = deferred_auth_ctx->krb5_ctx;
                auth_data->be_ctx = deferred_auth_ctx->be_ctx;

                te = tevent_add_timer(deferred_auth_ctx->ev,
                                      auth_data, tevent_timeval_current(),
                                      authenticate_user, auth_data);
                if (te == NULL) {
                    DEBUG(1, ("tevent_add_timer failed.\n"));
                }
            }
        } else {
            DEBUG(1, ("User [%s] is not logged in anymore, "
                      "discarding online authentication.\n", pd->user));
            talloc_free(pd);
        }

        ret = hash_delete(deferred_auth_ctx->user_table,
                          &entry->key);
        if (ret != HASH_SUCCESS) {
            DEBUG(1, ("hash_delete failed [%s].\n",
                      hash_error_string(ret)));
        }
    }

    talloc_free(iter);

    return EOK;
}

static void delayed_online_authentication_callback(void *private_data)
{
    struct deferred_auth_ctx *deferred_auth_ctx =
            talloc_get_type(private_data, struct deferred_auth_ctx);
    int ret;

    if (deferred_auth_ctx->user_table == NULL) {
        DEBUG(1, ("Delayed online authentication activated, "
                  "but user table does not exists.\n"));
        return;
    }

    DEBUG(5, ("Backend is online, starting delayed online authentication.\n"));
    ret = authenticate_stored_users(deferred_auth_ctx);
    if (ret != EOK) {
        DEBUG(1, ("authenticate_stored_users failed.\n"));
    }

    return;
}

errno_t add_user_to_delayed_online_authentication(struct krb5_ctx *krb5_ctx,
                                                  struct pam_data *pd,
                                                  uid_t uid)
{
    int ret;
    hash_key_t key;
    hash_value_t value;
    struct pam_data *new_pd;

    if (krb5_ctx->deferred_auth_ctx == NULL) {
        DEBUG(1, ("Missing context for delayed online authentication.\n"));
        return EINVAL;
    }

    if (krb5_ctx->deferred_auth_ctx->user_table == NULL) {
        DEBUG(1, ("user_table not available.\n"));
        return EINVAL;
    }

    if (pd->authtok_size == 0 || pd->authtok == NULL) {
        DEBUG(1, ("Missing authtok for user [%s].\n", pd->user));
        return EINVAL;
    }

    ret = copy_pam_data(krb5_ctx->deferred_auth_ctx, pd, &new_pd);
    if (ret != EOK) {
        DEBUG(1, ("copy_pam_data failed\n"));
        return ENOMEM;
    }


#ifdef USE_KEYRING
    new_pd->key_serial = add_key("user", new_pd->user, new_pd->authtok,
                                 new_pd->authtok_size, KEY_SPEC_SESSION_KEYRING);
    if (new_pd->key_serial == -1) {
        ret = errno;
        DEBUG(1, ("add_key fialed [%d][%s].\n", ret, strerror(ret)));
        talloc_free(new_pd);
        return ret;
    }
    DEBUG(9, ("Saved authtok of user [%s] with serial [%ld].\n",
              new_pd->user, new_pd->key_serial));
    memset(new_pd->authtok, 0, new_pd->authtok_size);
#endif

    key.type = HASH_KEY_ULONG;
    key.ul = uid;
    value.type = HASH_VALUE_PTR;
    value.ptr = new_pd;

    ret = hash_enter(krb5_ctx->deferred_auth_ctx->user_table,
                     &key, &value);
    if (ret != HASH_SUCCESS) {
        DEBUG(1, ("Cannot add user [%s] to table [%s], "
                  "delayed online authentication not possible.\n",
                  pd->user, hash_error_string(ret)));
        talloc_free(new_pd);
        return ENOMEM;
    }

    DEBUG(9, ("Added user [%s] successfully to "
              "delayed online authentication.\n", pd->user));

    return EOK;
}

errno_t init_delayed_online_authentication(struct krb5_ctx *krb5_ctx,
                                           struct be_ctx *be_ctx,
                                           struct tevent_context *ev)
{
    int ret;
    hash_table_t *tmp_table;

    ret = get_uid_table(krb5_ctx, &tmp_table);
    if (ret != EOK) {
        if (ret == ENOSYS) {
            DEBUG(0, ("Delayed online auth was requested "
                      "on an unsupported system.\n"));
        } else {
            DEBUG(0, ("Delayed online auth was requested "
                      "but initialisation failed.\n"));
        }
        return ret;
    }
    ret = hash_destroy(tmp_table);
    if (ret != HASH_SUCCESS) {
        DEBUG(1, ("hash_destroy failed [%s].\n", hash_error_string(ret)));
        return EFAULT;
    }

    krb5_ctx->deferred_auth_ctx = talloc_zero(krb5_ctx,
                                          struct deferred_auth_ctx);
    if (krb5_ctx->deferred_auth_ctx == NULL) {
        DEBUG(1, ("talloc_zero failed.\n"));
        return ENOMEM;
    }

    ret = hash_create_ex(INITIAL_USER_TABLE_SIZE,
                         &krb5_ctx->deferred_auth_ctx->user_table,
                         0, 0, 0, 0, hash_talloc, hash_talloc_free,
                         krb5_ctx->deferred_auth_ctx,
                         NULL, NULL);
    if (ret != HASH_SUCCESS) {
        DEBUG(1, ("hash_create_ex failed [%s]\n", hash_error_string(ret)));
        ret = ENOMEM;
        goto fail;
    }

    krb5_ctx->deferred_auth_ctx->be_ctx = be_ctx;
    krb5_ctx->deferred_auth_ctx->krb5_ctx = krb5_ctx;
    krb5_ctx->deferred_auth_ctx->ev = ev;

    ret = be_add_online_cb(krb5_ctx, be_ctx,
                           delayed_online_authentication_callback,
                           krb5_ctx->deferred_auth_ctx, NULL);
    if (ret != EOK) {
        DEBUG(1, ("be_add_online_cb failed.\n"));
        goto fail;
    }

    /* TODO: add destructor */

    return EOK;
fail:
    talloc_zfree(krb5_ctx->deferred_auth_ctx);
    return ret;
}