/*
   SSSD

   Configuration Database

   Copyright (C) Stephen Gallagher <sgallagh@redhat.com>	2009

   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 "config.h"
#include <sys/stat.h>
#include "util/util.h"
#include "db/sysdb.h"
#include "confdb.h"
#include "confdb_private.h"
#include "confdb_setup.h"
#include "util/sss_ini.h"

#ifndef SSSD_FALLBACK_CONFIG_LDIF
#define SSSD_FALLBACK_CONFIG_LDIF \
"dn: cn=config\n" \
"version: 2\n\n" \
"dn: cn=sssd,cn=config\n" \
"cn: sssd\n" \
"services: nss\n\n"
#endif /* SSSD_FALLBACK_CONFIG_LDIF */

static int confdb_test(struct confdb_ctx *cdb)
{
    char **values;
    int ret;

    ret = confdb_get_param(cdb, cdb,
                           "config",
                           "version",
                           &values);
    if (ret != EOK) {
        return ret;
    }

    if (values[0] == NULL) {
        /* empty database, will need to init */
        talloc_free(values);
        return ENOENT;
    }

    if (values[1] != NULL) {
        /* more than 1 value?? */
        talloc_free(values);
        return EIO;
    }

    if (strcmp(values[0], CONFDB_VERSION) != 0) {
        /* Existing version does not match executable version */
        DEBUG(SSSDBG_CRIT_FAILURE, "Upgrading confdb version from %s to %s\n",
                  values[0], CONFDB_VERSION);

        /* This is recoverable, since we purge the confdb file
         * when we re-initialize it.
         */
        talloc_free(values);
        return ENOENT;
    }

    talloc_free(values);
    return EOK;
}

static int confdb_purge(struct confdb_ctx *cdb)
{
    int ret;
    unsigned int i;
    TALLOC_CTX *tmp_ctx;
    struct ldb_result *res;
    struct ldb_dn *dn;
    const char *attrs[] = { "dn", NULL };

    tmp_ctx = talloc_new(NULL);

    dn = ldb_dn_new(tmp_ctx, cdb->ldb, "cn=config");

    /* Get the list of all DNs */
    ret = ldb_search(cdb->ldb, tmp_ctx, &res, dn,
                     LDB_SCOPE_SUBTREE, attrs, NULL);
    if (ret != LDB_SUCCESS) {
        ret = sysdb_error_to_errno(ret);
        goto done;
    }

    for(i=0; i<res->count; i++) {
        /* Delete this DN */
        ret = ldb_delete(cdb->ldb, res->msgs[i]->dn);
        if (ret != LDB_SUCCESS) {
            ret = sysdb_error_to_errno(ret);
            goto done;
        }
    }

done:
    talloc_free(tmp_ctx);
    return ret;
}

static int confdb_create_base(struct confdb_ctx *cdb)
{
    int ret;
    struct ldb_ldif *ldif;

    const char *base_ldif = CONFDB_BASE_LDIF;

    while ((ldif = ldb_ldif_read_string(cdb->ldb, &base_ldif))) {
        ret = ldb_add(cdb->ldb, ldif->msg);
        if (ret != LDB_SUCCESS) {
            DEBUG(SSSDBG_FATAL_FAILURE,
                  "Failed to initialize DB (%d,[%s]), aborting!\n",
                      ret, ldb_errstring(cdb->ldb));
            return EIO;
        }
        ldb_ldif_read_free(cdb->ldb, ldif);
    }

    return EOK;
}

static int confdb_ldif_from_ini_file(TALLOC_CTX *mem_ctx,
                                     const char *config_file,
                                     const char *config_dir,
                                     struct sss_ini_initdata *init_data,
                                     const char **_timestr,
                                     const char **_ldif)
{
    errno_t ret;
    char timestr[21];
    int version;

    ret = sss_ini_config_access_check(init_data);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Permission check on config file failed.\n");
        return EPERM;
    }

    ret = sss_ini_get_stat(init_data);
    if (ret != EOK) {
        ret = errno;
        DEBUG(SSSDBG_FATAL_FAILURE,
              "Status check on config file failed.\n");
        return ret;
    }

    errno = 0;
    ret = sss_ini_get_mtime(init_data, sizeof(timestr), timestr);
    if (ret <= 0 || ret >= (int)sizeof(timestr)) {
        DEBUG(SSSDBG_FATAL_FAILURE,
              "Failed to convert time_t to string??\n");
        ret = errno ? errno : EFAULT;
        return ret;
    }

    /* FIXME: Determine if the conf file or any snippet has changed
     * since we last updated the confdb or if some snippet was
     * added or removed.
     */

    ret = sss_ini_get_config(init_data, config_file, config_dir);
    if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Failed to load configuration\n");
        return ret;
    }

    ret = sss_ini_call_validators(init_data,
                                  SSSDDATADIR"/cfg_rules.ini");
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to call validators\n");
        /* This is not fatal, continue */
    }

    /* Make sure that the config file version matches the confdb version */
    ret = sss_ini_get_cfgobj(init_data, "sssd", "config_file_version");
    if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE,
              "Internal error determining config_file_version\n");
        return ret;
    }

    ret = sss_ini_check_config_obj(init_data);
    if (ret != EOK) {
        /* No known version. Use default. */
        DEBUG(SSSDBG_CONF_SETTINGS,
              "Value of config_file_version option not found. "
              "Assumed to be version %d.\n", CONFDB_DEFAULT_CFG_FILE_VER);
    } else {
        version = sss_ini_get_int_config_value(init_data,
                                               CONFDB_DEFAULT_CFG_FILE_VER,
                                               -1, &ret);
        if (ret != EOK) {
            DEBUG(SSSDBG_FATAL_FAILURE,
                  "Config file version could not be determined\n");
            return ret;
        } else if (version < CONFDB_VERSION_INT) {
            DEBUG(SSSDBG_FATAL_FAILURE,
                  "Config file is an old version. "
                  "Please run configuration upgrade script.\n");
            return EINVAL;
        } else if (version > CONFDB_VERSION_INT) {
            DEBUG(SSSDBG_FATAL_FAILURE,
                  "Config file version is newer than confdb\n");
            return EINVAL;
        }
    }

    ret = sss_confdb_create_ldif(mem_ctx, init_data, _ldif);
    if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Could not create LDIF for confdb\n");
        return ret;
    }

    *_timestr = talloc_strdup(mem_ctx, timestr);
    if (*_timestr == NULL) {
        return ENOMEM;
    }

    return EOK;
}

static int confdb_fallback_ldif(TALLOC_CTX *mem_ctx,
                                const char **_timestr,
                                const char **_ldif)
{
    *_timestr = talloc_strdup(mem_ctx, "1");
    *_ldif = talloc_strdup(mem_ctx, SSSD_FALLBACK_CONFIG_LDIF);
    if (*_timestr == NULL || *_ldif == NULL) {
        return ENOMEM;
    }

    return EOK;
}

static int confdb_init_db(const char *config_file, const char *config_dir,
                          struct confdb_ctx *cdb)
{
    TALLOC_CTX *tmp_ctx;
    int ret;
    int sret = EOK;
    bool in_transaction = false;
    const char *timestr = NULL;
    const char *config_ldif;
    const char *vals[2] = { NULL, NULL };
    struct ldb_ldif *ldif;
    struct sss_ini_initdata *init_data;

    tmp_ctx = talloc_new(cdb);
    if (tmp_ctx == NULL) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Out of memory.\n");
        return ENOMEM;
    }

    init_data = sss_ini_initdata_init(tmp_ctx);
    if (!init_data) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Out of memory.\n");
        ret = ENOMEM;
        goto done;
    }

    /* Open config file */
    ret = sss_ini_config_file_open(init_data, config_file);
    if (ret == EOK) {
        ret = confdb_ldif_from_ini_file(tmp_ctx,
                                        config_file,
                                        config_dir,
                                        init_data,
                                        &timestr,
                                        &config_ldif);
        if (ret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "Cannot convert INI to LDIF [%d]: [%s]\n",
                  ret, sss_strerror(ret));
            goto done;
        }
    } else if (ret == ENOENT) {
        ret = confdb_fallback_ldif(tmp_ctx, &timestr, &config_ldif);
        if (ret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "Cannot create a fallback configuration [%d]: [%s]\n",
                  ret, sss_strerror(ret));
            goto done;
        }
    } else {
        DEBUG(SSSDBG_CONF_SETTINGS,
              "sss_ini_config_file_open failed: %s [%d]\n", sss_strerror(ret),
              ret);
        goto done;
    }

    DEBUG(SSSDBG_CONF_SETTINGS, "LDIF file to import: \n%s\n", config_ldif);

    /* Set up a transaction to replace the configuration */
    ret = ldb_transaction_start(cdb->ldb);
    if (ret != LDB_SUCCESS) {
        DEBUG(SSSDBG_FATAL_FAILURE,
              "Failed to start a transaction for "
               "updating the configuration\n");
        ret = sysdb_error_to_errno(ret);
        goto done;
    }
    in_transaction = true;

    /* Purge existing database */
    ret = confdb_purge(cdb);
    if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE,
              "Could not purge existing configuration\n");
        goto done;
    }

    while ((ldif = ldb_ldif_read_string(cdb->ldb, &config_ldif))) {
        ret = ldb_add(cdb->ldb, ldif->msg);
        if (ret != LDB_SUCCESS) {
            DEBUG(SSSDBG_FATAL_FAILURE,
                  "Failed to initialize DB (%d,[%s]), aborting!\n",
                  ret, ldb_errstring(cdb->ldb));
            ret = EIO;
            goto done;
        }
        ldb_ldif_read_free(cdb->ldb, ldif);
    }

    /* now store the lastUpdate time so that we do not re-init if nothing
     * changed on restart */

    vals[0] = timestr;
    ret = confdb_add_param(cdb, true, "config", "lastUpdate", vals);
    if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE,
              "Failed to set last update time on db!\n");
        goto done;
    }

    ret = ldb_transaction_commit(cdb->ldb);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to commit transaction\n");
        goto done;
    }
    in_transaction = false;

    ret = EOK;

done:
    if (in_transaction) {
        sret = ldb_transaction_cancel(cdb->ldb);
        if (sret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE, "Failed to cancel transaction\n");
        }
    }

    sss_ini_config_destroy(init_data);
    sss_ini_close_file(init_data);

    talloc_zfree(tmp_ctx);
    return ret;
}

errno_t confdb_setup(TALLOC_CTX *mem_ctx,
                     const char *cdb_file,
                     const char *config_file,
                     const char *config_dir,
                     struct confdb_ctx **_cdb)
{
    TALLOC_CTX *tmp_ctx;
    struct confdb_ctx *cdb;
    errno_t ret;

    tmp_ctx = talloc_new(NULL);
    if (tmp_ctx == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc_new() failed\n");
        return ENOMEM;
    }

    ret = confdb_init(tmp_ctx, &cdb, cdb_file);
    if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE, "The confdb initialization failed "
              "[%d]: %s\n", ret, sss_strerror(ret));
        goto done;
    }

    /* Initialize the CDB from the configuration file */
    ret = confdb_test(cdb);
    if (ret == ENOENT) {
        /* First-time setup */

        /* Purge any existing confdb in case an old
         * misconfiguration gets in the way
         */
        talloc_zfree(cdb);
        ret = unlink(cdb_file);
        if (ret != EOK && errno != ENOENT) {
            ret = errno;
            DEBUG(SSSDBG_MINOR_FAILURE,
                  "Purging existing confdb failed: %d [%s].\n",
                  ret, sss_strerror(ret));
            goto done;
        }

        ret = confdb_init(tmp_ctx, &cdb, cdb_file);
        if (ret != EOK) {
            DEBUG(SSSDBG_FATAL_FAILURE, "The confdb initialization failed "
                  "[%d]: %s\n", ret, sss_strerror(ret));
        }

        /* Load special entries */
        ret = confdb_create_base(cdb);
        if (ret != EOK) {
            DEBUG(SSSDBG_FATAL_FAILURE,
                  "Unable to load special entries into confdb\n");
            goto done;
        }
    } else if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Fatal error initializing confdb\n");
        goto done;
    }

    ret = confdb_init_db(config_file, config_dir, cdb);
    if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE, "ConfDB initialization has failed "
              "[%d]: %s\n", ret, sss_strerror(ret));
        goto done;
    }

    *_cdb = talloc_steal(mem_ctx, cdb);

    ret = EOK;

done:
    talloc_free(tmp_ctx);
    return ret;
}
