/*
   SSSD

   NSS Responder

   Copyright (C) Simo Sorce <ssorce@redhat.com>	2008

   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 <fcntl.h>
#include <time.h>
#include "tdb.h"
#include "util/util.h"
#include "util/nss_dl_load.h"
#include "confdb/confdb.h"
#include "responder/common/negcache_files.h"
#include "responder/common/responder.h"
#include "responder/common/negcache.h"


#define NC_ENTRY_PREFIX "NCE/"
#define NC_USER_PREFIX NC_ENTRY_PREFIX"USER"
#define NC_GROUP_PREFIX NC_ENTRY_PREFIX"GROUP"
#define NC_NETGROUP_PREFIX NC_ENTRY_PREFIX"NETGR"
#define NC_SERVICE_PREFIX NC_ENTRY_PREFIX"SERVICE"
#define NC_UID_PREFIX NC_ENTRY_PREFIX"UID"
#define NC_GID_PREFIX NC_ENTRY_PREFIX"GID"
#define NC_SID_PREFIX NC_ENTRY_PREFIX"SID"
#define NC_CERT_PREFIX NC_ENTRY_PREFIX"CERT"
#define NC_DOMAIN_ACCT_LOCATE_PREFIX NC_ENTRY_PREFIX"DOM_LOCATE"
#define NC_DOMAIN_ACCT_LOCATE_TYPE_PREFIX NC_ENTRY_PREFIX"DOM_LOCATE_TYPE"

struct sss_nc_ctx {
    struct tdb_context *tdb;
    uint32_t timeout;
    uint32_t local_timeout;
    struct sss_nss_ops ops;
};

typedef int (*ncache_set_byname_fn_t)(struct sss_nc_ctx *, bool,
                                      const char *, const char *);

static int sss_ncache_set_ent(struct sss_nc_ctx *ctx, bool permanent,
                              struct sss_domain_info *dom, const char *name,
                              ncache_set_byname_fn_t setter);

static int string_to_tdb_data(char *str, TDB_DATA *ret)
{
    if (!str || !ret) return EINVAL;

    ret->dptr = (uint8_t *)str;
    ret->dsize = strlen(str)+1;

    return EOK;
}

static errno_t ncache_load_nss_symbols(struct sss_nss_ops *ops)
{
    errno_t ret;
    struct sss_nss_symbols syms[] = {
        {(void*)&ops->getpwnam_r, true, "getpwnam_r" },
        {(void*)&ops->getpwuid_r, true, "getpwuid_r" },
        {(void*)&ops->getgrnam_r, true, "getgrnam_r" },
        {(void*)&ops->getgrgid_r, true, "getgrgid_r" }
    };
    size_t nsyms = sizeof(syms) / sizeof(struct sss_nss_symbols);

    ret = sss_load_nss_symbols(ops, "files", syms, nsyms);
    if (ret != EOK) {
        return ret;
    }

    return EOK;
}

int sss_ncache_init(TALLOC_CTX *memctx, uint32_t timeout,
                    uint32_t local_timeout, struct sss_nc_ctx **_ctx)
{
    errno_t ret;
    struct sss_nc_ctx *ctx;

    ctx = talloc_zero(memctx, struct sss_nc_ctx);
    if (!ctx) return ENOMEM;

    ret = ncache_load_nss_symbols(&ctx->ops);
    if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Unable to load NSS symbols [%d]: %s\n",
              ret, sss_strerror(ret));
        talloc_free(ctx);
        return ret;
    }

    errno = 0;
    /* open a memory only tdb with default hash size */
    ctx->tdb = tdb_open("memcache", 0, TDB_INTERNAL, O_RDWR|O_CREAT, 0);
    if (!ctx->tdb) return errno;

    ctx->timeout = timeout;
    ctx->local_timeout = local_timeout;

    *_ctx = ctx;
    return EOK;
};

uint32_t sss_ncache_get_timeout(struct sss_nc_ctx *ctx)
{
    return ctx->timeout;
}

static int sss_ncache_check_str(struct sss_nc_ctx *ctx, char *str)
{
    TDB_DATA key;
    TDB_DATA data;
    unsigned long long int timestamp;
    bool expired = false;
    char *ep;
    int ret;

    DEBUG(SSSDBG_TRACE_INTERNAL, "Checking negative cache for [%s]\n", str);

    data.dptr = NULL;

    ret = string_to_tdb_data(str, &key);
    if (ret != EOK) goto done;

    data = tdb_fetch(ctx->tdb, key);

    if (!data.dptr) {
        ret = ENOENT;
        goto done;
    }

    errno = 0;
    timestamp = strtoull((const char *)data.dptr, &ep, 10);
    if (errno != 0 || *ep != '\0') {
        /* Malformed entry, remove it and return no entry */
        expired = true;
        goto done;
    }

    if (timestamp == 0) {
        /* a 0 timestamp means this is a permanent entry */
        ret = EEXIST;
        goto done;
    }

    if (timestamp >= time(NULL)) {
        /* still valid */
        ret = EEXIST;
        goto done;
    }

    expired = true;

done:
    if (expired) {
        /* expired, remove and return no entry */
        tdb_delete(ctx->tdb, key);
        ret = ENOENT;
    }

    free(data.dptr);
    return ret;
}

static int sss_ncache_set_str(struct sss_nc_ctx *ctx, char *str,
                              bool permanent, bool use_local_negative)
{
    TDB_DATA key;
    TDB_DATA data;
    char *timest;
    unsigned long long int timell;
    int ret;

    ret = string_to_tdb_data(str, &key);
    if (ret != EOK) return ret;

    if (permanent) {
        timest = talloc_strdup(ctx, "0");
    } else {
        if (use_local_negative == true && ctx->local_timeout > ctx->timeout) {
            timell = ctx->local_timeout;
        } else {
            /* EOK is tested in cwrap based unit test */
            if (ctx->timeout == 0) {
                return EOK;
            }
            timell = ctx->timeout;
        }
        timell += (unsigned long long int)time(NULL);
        timest = talloc_asprintf(ctx, "%llu", timell);
    }
    if (!timest) return ENOMEM;

    ret = string_to_tdb_data(timest, &data);
    if (ret != EOK) goto done;

    DEBUG(SSSDBG_TRACE_FUNC, "Adding [%s] to negative cache%s\n",
              str, permanent?" permanently":"");

    ret = tdb_store(ctx->tdb, key, data, TDB_REPLACE);
    if (ret != 0) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Negative cache failed to set entry: [%s]\n",
                  tdb_errorstr(ctx->tdb));
        ret = EFAULT;
    }

done:
    talloc_free(timest);
    return ret;
}

static int sss_ncache_check_user_int(struct sss_nc_ctx *ctx, const char *domain,
                                     const char *name)
{
    char *str;
    int ret;

    if (!name || !*name) return EINVAL;

    str = talloc_asprintf(ctx, "%s/%s/%s", NC_USER_PREFIX, domain, name);
    if (!str) return ENOMEM;

    ret = sss_ncache_check_str(ctx, str);

    talloc_free(str);
    return ret;
}

static int sss_ncache_check_group_int(struct sss_nc_ctx *ctx,
                                      const char *domain, const char *name)
{
    char *str;
    int ret;

    if (!name || !*name) return EINVAL;

    str = talloc_asprintf(ctx, "%s/%s/%s", NC_GROUP_PREFIX, domain, name);
    if (!str) return ENOMEM;

    ret = sss_ncache_check_str(ctx, str);

    talloc_free(str);
    return ret;
}

static int sss_ncache_check_netgr_int(struct sss_nc_ctx *ctx,
                                      const char *domain, const char *name)
{
    char *str;
    int ret;

    if (!name || !*name) return EINVAL;

    str = talloc_asprintf(ctx, "%s/%s/%s", NC_NETGROUP_PREFIX, domain, name);
    if (!str) return ENOMEM;

    ret = sss_ncache_check_str(ctx, str);

    talloc_free(str);
    return ret;
}

static int sss_ncache_check_service_int(struct sss_nc_ctx *ctx,
                                        const char *domain,
                                        const char *name)
{
    char *str;
    int ret;

    if (!name || !*name) return EINVAL;

    str = talloc_asprintf(ctx, "%s/%s/%s",
                          NC_SERVICE_PREFIX,
                          domain,
                          name);
    if (!str) return ENOMEM;

    ret = sss_ncache_check_str(ctx, str);

    talloc_free(str);
    return ret;
}

typedef int (*ncache_check_byname_fn_t)(struct sss_nc_ctx *, const char *,
                                        const char *);

static int sss_cache_check_ent(struct sss_nc_ctx *ctx,
                               struct sss_domain_info *dom, const char *name,
                               ncache_check_byname_fn_t checker)
{
    char *lower;
    errno_t ret;

    if (dom->case_sensitive == false) {
        lower = sss_tc_utf8_str_tolower(ctx, name);
        if (!lower) return ENOMEM;
        ret = checker(ctx, dom->name, lower);
        talloc_free(lower);
    } else {
        ret = checker(ctx, dom->name, name);
    }

    return ret;
}

int sss_ncache_check_user(struct sss_nc_ctx *ctx, struct sss_domain_info *dom,
                          const char *name)
{
    return sss_cache_check_ent(ctx, dom, name, sss_ncache_check_user_int);
}

int sss_ncache_check_upn(struct sss_nc_ctx *ctx, struct sss_domain_info *dom,
                         const char *name)
{
    char *neg_cache_name = NULL;
    errno_t ret;

    neg_cache_name = talloc_asprintf(ctx, "@%s", name);
    if (neg_cache_name == NULL) {
        return ENOMEM;
    }

    ret = sss_cache_check_ent(ctx, dom, neg_cache_name,
                              sss_ncache_check_user_int);
    talloc_free(neg_cache_name);

    return ret;
}

int sss_ncache_check_group(struct sss_nc_ctx *ctx, struct sss_domain_info *dom,
                           const char *name)
{
    return sss_cache_check_ent(ctx, dom, name, sss_ncache_check_group_int);
}

int sss_ncache_check_netgr(struct sss_nc_ctx *ctx, struct sss_domain_info *dom,
                           const char *name)
{
    return sss_cache_check_ent(ctx, dom, name, sss_ncache_check_netgr_int);
}

static int sss_ncache_set_service_int(struct sss_nc_ctx *ctx, bool permanent,
                                      const char *domain, const char *name)
{
    char *str;
    int ret;

    if (!name || !*name) return EINVAL;

    str = talloc_asprintf(ctx, "%s/%s/%s", NC_SERVICE_PREFIX, domain, name);
    if (!str) return ENOMEM;

    ret = sss_ncache_set_str(ctx, str, permanent, false);

    talloc_free(str);
    return ret;
}

int sss_ncache_set_service_name(struct sss_nc_ctx *ctx, bool permanent,
                                struct sss_domain_info *dom,
                                const char *name, const char *proto)
{
    int ret;
    char *service_and_protocol = talloc_asprintf(ctx, "%s:%s",
                                                 name,
                                                 proto ? proto : "<ANY>");
    if (!service_and_protocol) return ENOMEM;

    ret = sss_ncache_set_ent(ctx, permanent, dom,
                             service_and_protocol,
                             sss_ncache_set_service_int);
    talloc_free(service_and_protocol);
    return ret;
}

int sss_ncache_check_service(struct sss_nc_ctx *ctx,struct sss_domain_info *dom,
                             const char *name, const char *proto)
{
    int ret;
    char *service_and_protocol = talloc_asprintf(ctx, "%s:%s",
                                                 name,
                                                 proto ? proto : "<ANY>");
    if (!service_and_protocol) return ENOMEM;

    ret = sss_cache_check_ent(ctx, dom, service_and_protocol,
                              sss_ncache_check_service_int);
    talloc_free(service_and_protocol);
    return ret;
}

int sss_ncache_set_service_port(struct sss_nc_ctx *ctx, bool permanent,
                                struct sss_domain_info *dom,
                                uint16_t port, const char *proto)
{
    int ret;
    char *service_and_protocol = talloc_asprintf(ctx, "%ul:%s",
                                                 port,
                                                 proto ? proto : "<ANY>");
    if (!service_and_protocol) return ENOMEM;

    ret = sss_ncache_set_ent(ctx, permanent, dom,
                             service_and_protocol,
                             sss_ncache_set_service_int);
    talloc_free(service_and_protocol);
    return ret;
}

int sss_ncache_check_service_port(struct sss_nc_ctx *ctx,
                                  struct sss_domain_info *dom,
                                  uint16_t port,
                                  const char *proto)
{
    int ret;
    char *service_and_protocol = talloc_asprintf(ctx, "%ul:%s",
                                                 port,
                                                 proto ? proto : "<ANY>");
    if (!service_and_protocol) return ENOMEM;

    ret = sss_cache_check_ent(ctx, dom, service_and_protocol,
                              sss_ncache_check_service_int);
    talloc_free(service_and_protocol);
    return ret;
}



int sss_ncache_check_uid(struct sss_nc_ctx *ctx, struct sss_domain_info *dom,
                         uid_t uid)
{
    char *str;
    int ret;

    if (dom != NULL) {
        str = talloc_asprintf(ctx, "%s/%s/%"SPRIuid, NC_UID_PREFIX, dom->name,
                              uid);
    } else {
        str = talloc_asprintf(ctx, "%s/%"SPRIuid, NC_UID_PREFIX, uid);
    }
    if (!str) return ENOMEM;

    ret = sss_ncache_check_str(ctx, str);

    talloc_free(str);
    return ret;
}

int sss_ncache_check_gid(struct sss_nc_ctx *ctx, struct sss_domain_info *dom,
                         gid_t gid)
{
    char *str;
    int ret;

    if (dom != NULL) {
        str = talloc_asprintf(ctx, "%s/%s/%"SPRIgid, NC_GID_PREFIX, dom->name,
                              gid);
    } else {
        str = talloc_asprintf(ctx, "%s/%"SPRIgid, NC_GID_PREFIX, gid);
    }
    if (!str) return ENOMEM;

    ret = sss_ncache_check_str(ctx, str);

    talloc_free(str);
    return ret;
}

int sss_ncache_check_sid(struct sss_nc_ctx *ctx, const char *sid)
{
    char *str;
    int ret;

    str = talloc_asprintf(ctx, "%s/%s", NC_SID_PREFIX, sid);
    if (!str) return ENOMEM;

    ret = sss_ncache_check_str(ctx, str);

    talloc_free(str);
    return ret;
}

int sss_ncache_check_cert(struct sss_nc_ctx *ctx, const char *cert)
{
    char *str;
    int ret;

    str = talloc_asprintf(ctx, "%s/%s", NC_CERT_PREFIX, cert);
    if (!str) return ENOMEM;

    ret = sss_ncache_check_str(ctx, str);

    talloc_free(str);
    return ret;
}


static int sss_ncache_set_user_int(struct sss_nc_ctx *ctx, bool permanent,
                                   const char *domain, const char *name)
{
    bool use_local_negative = false;
    char *str;
    int ret;

    if (!name || !*name) return EINVAL;

    str = talloc_asprintf(ctx, "%s/%s/%s", NC_USER_PREFIX, domain, name);
    if (!str) return ENOMEM;

    if ((!permanent) && (ctx->local_timeout > 0)) {
        use_local_negative = is_user_local_by_name(&ctx->ops, name);
    }
    ret = sss_ncache_set_str(ctx, str, permanent, use_local_negative);

    talloc_free(str);
    return ret;
}

static int sss_ncache_set_group_int(struct sss_nc_ctx *ctx, bool permanent,
                                    const char *domain, const char *name)
{
    bool use_local_negative = false;
    char *str;
    int ret;

    if (!name || !*name) return EINVAL;

    str = talloc_asprintf(ctx, "%s/%s/%s", NC_GROUP_PREFIX, domain, name);
    if (!str) return ENOMEM;

    if ((!permanent) && (ctx->local_timeout > 0)) {
        use_local_negative = is_group_local_by_name(&ctx->ops, name);
    }
    ret = sss_ncache_set_str(ctx, str, permanent, use_local_negative);

    talloc_free(str);
    return ret;
}

static int sss_ncache_set_netgr_int(struct sss_nc_ctx *ctx, bool permanent,
                                    const char *domain, const char *name)
{
    char *str;
    int ret;

    if (!name || !*name) return EINVAL;

    str = talloc_asprintf(ctx, "%s/%s/%s", NC_NETGROUP_PREFIX, domain, name);
    if (!str) return ENOMEM;

    ret = sss_ncache_set_str(ctx, str, permanent, false);

    talloc_free(str);
    return ret;
}

static int sss_ncache_set_ent(struct sss_nc_ctx *ctx, bool permanent,
                              struct sss_domain_info *dom, const char *name,
                              ncache_set_byname_fn_t setter)
{
    char *lower;
    errno_t ret;

    if (dom->case_sensitive == false) {
        lower = sss_tc_utf8_str_tolower(ctx, name);
        if (!lower) return ENOMEM;
        ret = setter(ctx, permanent, dom->name, lower);
        talloc_free(lower);
    } else {
        ret = setter(ctx, permanent, dom->name, name);
    }

    return ret;
}


int sss_ncache_set_user(struct sss_nc_ctx *ctx, bool permanent,
                        struct sss_domain_info *dom, const char *name)
{
    return sss_ncache_set_ent(ctx, permanent, dom, name, sss_ncache_set_user_int);
}

int sss_ncache_set_upn(struct sss_nc_ctx *ctx, bool permanent,
                       struct sss_domain_info *dom, const char *name)
{
    char *neg_cache_name = NULL;
    errno_t ret;

    neg_cache_name = talloc_asprintf(ctx, "@%s", name);
    if (neg_cache_name == NULL) {
        return ENOMEM;
    }

    ret = sss_ncache_set_ent(ctx, permanent, dom, neg_cache_name,
                             sss_ncache_set_user_int);
    talloc_free(neg_cache_name);

    return ret;
}

int sss_ncache_set_group(struct sss_nc_ctx *ctx, bool permanent,
                         struct sss_domain_info *dom, const char *name)
{
    return sss_ncache_set_ent(ctx, permanent, dom, name, sss_ncache_set_group_int);
}

int sss_ncache_set_netgr(struct sss_nc_ctx *ctx, bool permanent,
                         struct sss_domain_info *dom, const char *name)
{
    return sss_ncache_set_ent(ctx, permanent, dom, name, sss_ncache_set_netgr_int);
}

int sss_ncache_set_uid(struct sss_nc_ctx *ctx, bool permanent,
                       struct sss_domain_info *dom, uid_t uid)
{
    bool use_local_negative = false;
    char *str;
    int ret;

    if (dom != NULL) {
        str = talloc_asprintf(ctx, "%s/%s/%"SPRIuid, NC_UID_PREFIX, dom->name,
                              uid);
    } else {
        str = talloc_asprintf(ctx, "%s/%"SPRIuid, NC_UID_PREFIX, uid);
    }
    if (!str) return ENOMEM;

    if ((!permanent) && (ctx->local_timeout > 0)) {
        use_local_negative = is_user_local_by_uid(&ctx->ops, uid);
    }
    ret = sss_ncache_set_str(ctx, str, permanent, use_local_negative);

    talloc_free(str);
    return ret;
}

int sss_ncache_set_gid(struct sss_nc_ctx *ctx, bool permanent,
                       struct sss_domain_info *dom, gid_t gid)
{
    bool use_local_negative = false;
    char *str;
    int ret;

    if (dom != NULL) {
        str = talloc_asprintf(ctx, "%s/%s/%"SPRIgid, NC_GID_PREFIX, dom->name,
                              gid);
    } else {
        str = talloc_asprintf(ctx, "%s/%"SPRIgid, NC_GID_PREFIX, gid);
    }
    if (!str) return ENOMEM;

    if ((!permanent) && (ctx->local_timeout > 0)) {
        use_local_negative = is_group_local_by_gid(&ctx->ops, gid);
    }
    ret = sss_ncache_set_str(ctx, str, permanent, use_local_negative);

    talloc_free(str);
    return ret;
}

int sss_ncache_set_sid(struct sss_nc_ctx *ctx, bool permanent, const char *sid)
{
    char *str;
    int ret;

    str = talloc_asprintf(ctx, "%s/%s", NC_SID_PREFIX, sid);
    if (!str) return ENOMEM;

    ret = sss_ncache_set_str(ctx, str, permanent, false);

    talloc_free(str);
    return ret;
}

int sss_ncache_set_cert(struct sss_nc_ctx *ctx, bool permanent,
                        const char *cert)
{
    char *str;
    int ret;

    str = talloc_asprintf(ctx, "%s/%s", NC_CERT_PREFIX, cert);
    if (!str) return ENOMEM;

    ret = sss_ncache_set_str(ctx, str, permanent, false);

    talloc_free(str);
    return ret;
}

static char *domain_lookup_type_str(TALLOC_CTX *mem_ctx,
                                    struct sss_domain_info *dom,
                                    const char *lookup_type)
{
    return talloc_asprintf(mem_ctx,
                           "%s/%s/%s",
                           NC_DOMAIN_ACCT_LOCATE_TYPE_PREFIX,
                           dom->name,
                           lookup_type);
}

int sss_ncache_set_domain_locate_type(struct sss_nc_ctx *ctx,
                                      struct sss_domain_info *dom,
                                      const char *lookup_type)
{
    char *str;
    int ret;

    str = domain_lookup_type_str(ctx, dom, lookup_type);
    if (!str) return ENOMEM;

    /* Permanent cache is always used here, because the lookup
     * type's (getgrgid, getpwuid, ..) support locating an entry's domain
     * doesn't change
     */
    ret = sss_ncache_set_str(ctx, str, true, false);
    talloc_free(str);
    return ret;
}

int sss_ncache_check_domain_locate_type(struct sss_nc_ctx *ctx,
                                        struct sss_domain_info *dom,
                                        const char *lookup_type)
{
    char *str;
    int ret;

    str = domain_lookup_type_str(ctx, dom, lookup_type);
    if (!str) return ENOMEM;

    ret = sss_ncache_check_str(ctx, str);
    talloc_free(str);
    return ret;
}

static char *locate_gid_str(TALLOC_CTX *mem_ctx,
                            struct sss_domain_info *dom,
                            gid_t gid)
{
    return talloc_asprintf(mem_ctx,
                           "%s/%s/%s/%"SPRIgid,
                           NC_DOMAIN_ACCT_LOCATE_PREFIX,
                           NC_GID_PREFIX,
                           dom->name,
                           gid);
}

int sss_ncache_set_locate_gid(struct sss_nc_ctx *ctx,
                              struct sss_domain_info *dom,
                              gid_t gid)
{
    char *str;
    int ret;

    if (dom == NULL) {
        return EINVAL;
    }

    str = locate_gid_str(ctx, dom, gid);
    if (str == NULL) {
        return ENOMEM;
    }

    ret = sss_ncache_set_str(ctx, str, false, false);
    talloc_free(str);
    return ret;
}

int sss_ncache_check_locate_gid(struct sss_nc_ctx *ctx,
                                struct sss_domain_info *dom,
                                gid_t gid)
{
    char *str;
    int ret;

    if (dom == NULL) {
        return EINVAL;
    }

    str = locate_gid_str(ctx, dom, gid);
    if (str == NULL) {
        return ENOMEM;
    }

    ret = sss_ncache_check_str(ctx, str);
    talloc_free(str);
    return ret;
}

static char *locate_uid_str(struct sss_nc_ctx *ctx,
                            struct sss_domain_info *dom,
                            uid_t uid)
{
    return talloc_asprintf(ctx,
                           "%s/%s/%s/%"SPRIuid,
                           NC_DOMAIN_ACCT_LOCATE_PREFIX,
                           NC_UID_PREFIX,
                           dom->name,
                           uid);
}

int sss_ncache_set_locate_uid(struct sss_nc_ctx *ctx,
                              struct sss_domain_info *dom,
                              uid_t uid)
{
    char *str;
    int ret;

    if (dom == NULL) {
        return EINVAL;
    }

    str = locate_uid_str(ctx, dom, uid);
    if (str == NULL) {
        return ENOMEM;
    }

    ret = sss_ncache_set_str(ctx, str, false, false);
    talloc_free(str);
    return ret;
}

int sss_ncache_check_locate_uid(struct sss_nc_ctx *ctx,
                                struct sss_domain_info *dom,
                                uid_t uid)
{
    char *str;
    int ret;

    if (dom == NULL) {
        return EINVAL;
    }

    str = locate_uid_str(ctx, dom, uid);
    if (str == NULL) {
        return ENOMEM;
    }

    ret = sss_ncache_check_str(ctx, str);
    talloc_free(str);
    return ret;
}

static int delete_permanent(struct tdb_context *tdb,
                            TDB_DATA key, TDB_DATA data, void *state)
{
    unsigned long long int timestamp;
    bool remove_key = false;
    char *ep;

    if (strncmp((char *)key.dptr,
                NC_ENTRY_PREFIX, sizeof(NC_ENTRY_PREFIX) - 1) != 0) {
        /* not interested in this key */
        return 0;
    }

    errno = 0;
    timestamp = strtoull((const char *)data.dptr, &ep, 10);
    if (errno != 0 || *ep != '\0') {
        /* Malformed entry, remove it */
        remove_key = true;
        goto done;
    }

    if (timestamp == 0) {
        /* a 0 timestamp means this is a permanent entry */
        remove_key = true;
    }

done:
    if (remove_key) {
        return tdb_delete(tdb, key);
    }

    return 0;
}

int sss_ncache_reset_permanent(struct sss_nc_ctx *ctx)
{
    int ret;

    ret = tdb_traverse(ctx->tdb, delete_permanent, NULL);
    if (ret < 0)
        return EIO;

    return EOK;
}

static int delete_prefix(struct tdb_context *tdb,
                         TDB_DATA key, TDB_DATA data, void *state)
{
    const char *prefix = (const char *) state;
    unsigned long long int timestamp;
    char *ep = NULL;

    if (strncmp((char *)key.dptr, prefix, strlen(prefix) - 1) != 0) {
        /* not interested in this key */
        return 0;
    }

    errno = 0;
    timestamp = strtoull((const char *)data.dptr, &ep, 10);
    if ((errno == 0) && (*ep == '\0') && (timestamp == 0)) {
        /* skip permanent entries */
        return 0;
    }

    return tdb_delete(tdb, key);
}

static int sss_ncache_reset_pfx(struct sss_nc_ctx *ctx,
                                const char **prefixes)
{
    int ret;

    if (prefixes == NULL) {
        return EOK;
    }

    for (int i = 0; prefixes[i] != NULL; i++) {
        ret = tdb_traverse(ctx->tdb,
                           delete_prefix,
                           discard_const(prefixes[i]));
        if (ret < 0) {
            return EIO;
        }
    }

    return EOK;
}

int sss_ncache_reset_users(struct sss_nc_ctx *ctx)
{
    const char *prefixes[] = {
        NC_USER_PREFIX,
        NC_UID_PREFIX,
        NULL,
    };

    return sss_ncache_reset_pfx(ctx, prefixes);
}

int sss_ncache_reset_groups(struct sss_nc_ctx *ctx)
{
    const char *prefixes[] = {
        NC_GROUP_PREFIX,
        NC_GID_PREFIX,
        NULL,
    };

    return sss_ncache_reset_pfx(ctx, prefixes);
}

errno_t sss_ncache_prepopulate(struct sss_nc_ctx *ncache,
                               struct confdb_ctx *cdb,
                               struct resp_ctx *rctx)
{
    errno_t ret;
    char **filter_list = NULL;
    char **default_list = NULL;
    char *name = NULL;
    struct sss_domain_info *dom = NULL;
    struct sss_domain_info *domain_list = rctx->domains;
    struct sss_domain_info *ddom;
    char *domainname = NULL;
    char *conf_path = NULL;
    TALLOC_CTX *tmpctx = talloc_new(NULL);
    int i;
    char *fqname = NULL;

    if (tmpctx == NULL) {
        return ENOMEM;
    }

    /* Populate domain-specific negative cache user entries */
    for (dom = domain_list; dom; dom = get_next_domain(dom, 0)) {
        conf_path = talloc_asprintf(tmpctx, CONFDB_DOMAIN_PATH_TMPL,
                                    dom->name);
        if (!conf_path) {
            ret = ENOMEM;
            goto done;
        }

        talloc_zfree(filter_list);
        ret = confdb_get_string_as_list(cdb, tmpctx, conf_path,
                                        CONFDB_NSS_FILTER_USERS,
                                        &filter_list);
        if (ret == ENOENT) continue;
        if (ret != EOK) goto done;

        for (i = 0; (filter_list && filter_list[i]); i++) {
            ret = sss_parse_name_for_domains(tmpctx, domain_list,
                                             NULL,
                                             filter_list[i],
                                             &domainname, &name);
            if (ret == EAGAIN) {
                DEBUG(SSSDBG_MINOR_FAILURE,
                      "Can add [%s] only as UPN to negcache because the "
                      "required domain is not known yet\n", filter_list[i]);
            } else if (ret != EOK) {
                DEBUG(SSSDBG_CRIT_FAILURE,
                      "Invalid name in filterUsers list: [%s] (%d)\n",
                         filter_list[i], ret);
                continue;
            }

            /* Check domain and its sub-domains */
            for (ddom = dom; ddom != NULL;
                        ddom = get_next_domain(ddom, SSS_GND_ALL_SUBDOMAINS)) {

                if (domainname && strcmp(domainname, ddom->name)) {
                    DEBUG(SSSDBG_TRACE_FUNC,
                          "Mismatch between domain name (%s) and name "
                              "set in FQN  (%s), assuming %s is UPN\n",
                              ddom->name, domainname, filter_list[i]);
                    ret = sss_ncache_set_upn(ncache, true, ddom, filter_list[i]);
                    if (ret != EOK) {
                        DEBUG(SSSDBG_OP_FAILURE,
                              "sss_ncache_set_upn failed (%d [%s]), ignored\n",
                              ret, sss_strerror(ret));
                    }
                    continue;
                }

                fqname = sss_create_internal_fqname(tmpctx, name, ddom->name);
                if (fqname == NULL) {
                    continue;
                }

                ret = sss_ncache_set_upn(ncache, true, ddom, fqname);
                if (ret != EOK) {
                    DEBUG(SSSDBG_OP_FAILURE,
                          "sss_ncache_set_upn failed (%d [%s]), ignored\n",
                          ret, sss_strerror(ret));
                }
                ret = sss_ncache_set_user(ncache, true, ddom, fqname);
                talloc_zfree(fqname);
                if (ret != EOK) {
                    DEBUG(SSSDBG_CRIT_FAILURE,
                          "Failed to store permanent user filter for [%s]"
                              " (%d [%s])\n", filter_list[i],
                              ret, sss_strerror(ret));
                    continue;
                }
            }
        }
    }

    talloc_zfree(filter_list);
    /* Populate non domain-specific negative cache user entries */
    ret = confdb_get_string_as_list(cdb, tmpctx, CONFDB_NSS_CONF_ENTRY,
                                    CONFDB_NSS_FILTER_USERS, &filter_list);
    if (ret != EOK && ret != ENOENT) {
        goto done;
    }

    for (i = 0; (filter_list && filter_list[i]); i++) {
        ret = sss_parse_name_for_domains(tmpctx, domain_list,
                                         NULL, filter_list[i],
                                         &domainname, &name);
        if (ret == EAGAIN) {
            DEBUG(SSSDBG_MINOR_FAILURE,
                  "Can add [%s] only as UPN to negcache because the "
                  "required domain is not known yet\n", filter_list[i]);
        } else if (ret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "Invalid name in filterUsers list: [%s] (%d)\n",
                     filter_list[i], ret);
            continue;
        }
        if (domainname) {
            DEBUG(SSSDBG_TRACE_ALL,
                  "Adding [%s] to UPN negative cache of all domains.\n",
                  filter_list[i]);
            for (dom = domain_list;
                 dom != NULL;
                 dom = get_next_domain(dom, SSS_GND_ALL_DOMAINS)) {
                ret = sss_ncache_set_upn(ncache, true, dom, filter_list[i]);
                if (ret != EOK) {
                    DEBUG(SSSDBG_OP_FAILURE,
                          "sss_ncache_set_upn failed (%d [%s]), ignored\n",
                          ret, sss_strerror(ret));
                }
            }

            /* Add name to domain specific cache for known domain names */
            dom = responder_get_domain(rctx, domainname);
            if (dom != NULL) {
                fqname = sss_create_internal_fqname(tmpctx, name, dom->name);
                if (fqname == NULL) {
                    continue;
                }

                ret = sss_ncache_set_user(ncache, true, dom, fqname);
                talloc_zfree(fqname);
                if (ret != EOK) {
                    DEBUG(SSSDBG_CRIT_FAILURE,
                          "Failed to store permanent user filter for [%s]"
                              " (%d [%s])\n", filter_list[i],
                              ret, strerror(ret));
                    continue;
                }
            }
        } else {
            for (dom = domain_list;
                 dom != NULL;
                 dom = get_next_domain(dom, SSS_GND_ALL_DOMAINS)) {
                fqname = sss_create_internal_fqname(tmpctx, name, dom->name);
                if (fqname == NULL) {
                    continue;
                }

                ret = sss_ncache_set_upn(ncache, true, dom, fqname);
                if (ret != EOK) {
                   DEBUG(SSSDBG_CRIT_FAILURE,
                         "Failed to store permanent upn filter for"
                             " [%s:%s] (%d [%s])\n",
                             dom->name, filter_list[i],
                             ret, strerror(ret));
                }

                ret = sss_ncache_set_user(ncache, true, dom, fqname);
                talloc_zfree(fqname);
                if (ret != EOK) {
                   DEBUG(SSSDBG_CRIT_FAILURE,
                         "Failed to store permanent user filter for"
                             " [%s:%s] (%d [%s])\n",
                             dom->name, filter_list[i],
                             ret, strerror(ret));
                    continue;
                }
            }
        }
    }

    /* Populate domain-specific negative cache group entries */
    for (dom = domain_list; dom; dom = get_next_domain(dom, 0)) {
        conf_path = talloc_asprintf(tmpctx, CONFDB_DOMAIN_PATH_TMPL, dom->name);
        if (!conf_path) {
            ret = ENOMEM;
            goto done;
        }

        talloc_zfree(filter_list);
        ret = confdb_get_string_as_list(cdb, tmpctx, conf_path,
                                        CONFDB_NSS_FILTER_GROUPS, &filter_list);
        if (ret == ENOENT) continue;
        if (ret != EOK) goto done;

        for (i = 0; (filter_list && filter_list[i]); i++) {
            ret = sss_parse_name_for_domains(tmpctx, domain_list,
                                             NULL, filter_list[i],
                                             &domainname, &name);
            if (ret != EOK) {
                /* Groups do not have UPNs, so domain names, if present,
                 * must be known */
                DEBUG(SSSDBG_CRIT_FAILURE,
                      "Invalid name in filterGroups list: [%s] (%d)\n",
                         filter_list[i], ret);
                continue;
            }

            /* Check domain and its sub-domains */
            for (ddom = dom;
                        ddom != NULL && (ddom == dom || ddom->parent != NULL);
                        ddom = get_next_domain(ddom, SSS_GND_ALL_DOMAINS)) {
                if (domainname && strcmp(domainname, ddom->name)) {
                    DEBUG(SSSDBG_CRIT_FAILURE,
                          "Mismatch between domain name (%s) and name "
                              "set in FQN  (%s), skipping group %s\n",
                              ddom->name, domainname, name);
                    continue;
                }

                fqname = sss_create_internal_fqname(tmpctx, name, ddom->name);
                if (fqname == NULL) {
                    continue;
                }

                ret = sss_ncache_set_group(ncache, true, ddom, fqname);
                talloc_zfree(fqname);
                if (ret != EOK) {
                    DEBUG(SSSDBG_CRIT_FAILURE,
                          "Failed to store permanent group filter for [%s]"
                              " (%d [%s])\n", filter_list[i],
                              ret, strerror(ret));
                    continue;
                }
            }
        }
    }

    talloc_zfree(filter_list);
    /* Populate non domain-specific negative cache group entries */
    ret = confdb_get_string_as_list(cdb, tmpctx, CONFDB_NSS_CONF_ENTRY,
                                    CONFDB_NSS_FILTER_GROUPS, &filter_list);
    if (ret != EOK && ret != ENOENT) {
        goto done;
    }

    for (i = 0; (filter_list && filter_list[i]); i++) {
        ret = sss_parse_name_for_domains(tmpctx, domain_list,
                                         NULL, filter_list[i],
                                         &domainname, &name);
        if (ret != EOK) {
            /* Groups do not have UPNs, so domain names, if present,
             * must be known */
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "Invalid name in filterGroups list: [%s] (%d)\n",
                     filter_list[i], ret);
            continue;
        }
        if (domainname) {
            dom = responder_get_domain(rctx, domainname);
            if (!dom) {
                DEBUG(SSSDBG_CRIT_FAILURE,
                      "Invalid domain name [%s]\n", domainname);
                continue;
            }

            fqname = sss_create_internal_fqname(tmpctx, name, dom->name);
            if (fqname == NULL) {
                continue;
            }

            ret = sss_ncache_set_group(ncache, true, dom, fqname);
            talloc_zfree(fqname);
            if (ret != EOK) {
                DEBUG(SSSDBG_CRIT_FAILURE,
                      "Failed to store permanent group filter for"
                          " [%s] (%d [%s])\n", filter_list[i],
                          ret, strerror(ret));
                continue;
            }
        } else {
            for (dom = domain_list;
                 dom != NULL;
                 dom = get_next_domain(dom, SSS_GND_ALL_DOMAINS)) {
                fqname = sss_create_internal_fqname(tmpctx, name, dom->name);
                if (fqname == NULL) {
                    continue;
                }

                ret = sss_ncache_set_group(ncache, true, dom, fqname);
                talloc_zfree(fqname);
                if (ret != EOK) {
                   DEBUG(SSSDBG_CRIT_FAILURE,
                         "Failed to store permanent group filter for"
                             " [%s:%s] (%d [%s])\n",
                             dom->name, filter_list[i],
                             ret, strerror(ret));
                    continue;
                }
            }
        }
    }

    /* SSSD doesn't handle "root", thus it'll be added to the negative cache
     * nonetheless what's already added there. */
    default_list = talloc_array(tmpctx, char *, 2);
    if (default_list == NULL) {
        ret= ENOMEM;
        goto done;
    }
    default_list[0] = talloc_strdup(tmpctx, "root");
    if (default_list[0] == NULL) {
        ret = ENOMEM;
        goto done;
    }
    default_list[1] = NULL;

    /* Populate negative cache users and groups entries for the
     * "default_list" */
    for (i = 0; (default_list != NULL && default_list[i] != NULL); i++) {
        for (dom = domain_list;
             dom != NULL;
             dom = get_next_domain(dom, SSS_GND_ALL_DOMAINS)) {
            fqname = sss_create_internal_fqname(tmpctx,
                                                default_list[i],
                                                dom->name);
            if (fqname == NULL) {
                continue;
            }

            ret = sss_ncache_set_user(ncache, true, dom, fqname);
            if (ret != EOK) {
                DEBUG(SSSDBG_MINOR_FAILURE,
                      "Failed to store permanent user filter for"
                      " [%s:%s] (%d [%s])\n",
                      dom->name, default_list[i],
                      ret, strerror(ret));
                continue;
            }

            ret = sss_ncache_set_group(ncache, true, dom, fqname);
            if (ret != EOK) {
                DEBUG(SSSDBG_MINOR_FAILURE,
                      "Failed to store permanent group filter for"
                      " [%s:%s] (%d [%s])\n",
                      dom->name, default_list[i],
                      ret, strerror(ret));
                continue;
            }
        }
    }

    /* Also add "root" uid and gid to the negative cache */
    ret = sss_ncache_set_uid(ncache, true, NULL, 0);
    if (ret != EOK) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              "Failed to store permanent uid filter for root (0) "
              "(%d [%s])\n",
              ret, strerror(ret));
    }

    ret = sss_ncache_set_gid(ncache, true, NULL, 0);
    if (ret != EOK) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              "Failed to store permanent gid filter for root (0) "
              "(%d [%s])\n",
              ret, strerror(ret));
    }

    ret = EOK;

done:
    talloc_free(tmpctx);
    return ret;
}

/* Reset permanent negcache after checking the domains */
errno_t sss_ncache_reset_repopulate_permanent(struct resp_ctx *rctx,
                                              struct sss_nc_ctx *ncache)
{
    int ret;

    ret = sss_ncache_reset_permanent(ncache);
    if (ret == EOK) {
        ret = sss_ncache_prepopulate(ncache, rctx->cdb, rctx);
    }

    return ret;
}
