/*
    SSSD

    proxy_id.c

    Authors:
        Stephen Gallagher <sgallagh@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 <dhash.h>
#include "config.h"

#include "util/sss_format.h"
#include "util/strtonum.h"
#include "providers/proxy/proxy.h"

/* =Getpwnam-wrapper======================================================*/

static int save_user(struct sss_domain_info *domain,
                     struct passwd *pwd, const char *real_name,
                     const char *alias);

static int
handle_getpw_result(enum nss_status status, struct passwd *pwd,
                    struct sss_domain_info *dom, bool *del_user);

static int
delete_user(struct sss_domain_info *domain,
            const char *name, uid_t uid);

int get_pw_name(struct proxy_id_ctx *ctx,
                struct sss_domain_info *dom,
                const char *i_name)
{
    TALLOC_CTX *tmpctx;
    struct passwd *pwd;
    enum nss_status status;
    char *buffer;
    size_t buflen;
    int ret;
    uid_t uid;
    bool del_user;
    struct ldb_result *cached_pwd = NULL;
    const char *real_name = NULL;
    char *shortname_or_alias;

    DEBUG(SSSDBG_TRACE_FUNC, "Searching user by name (%s)\n", i_name);

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

    ret = sss_parse_internal_fqname(tmpctx, i_name, &shortname_or_alias, NULL);
    if (ret != EOK) {
        goto done;
    }

    pwd = talloc_zero(tmpctx, struct passwd);
    if (!pwd) {
        ret = ENOMEM;
        goto done;
    }

    buflen = DEFAULT_BUFSIZE;
    buffer = talloc_size(tmpctx, buflen);
    if (!buffer) {
        ret = ENOMEM;
        goto done;
    }

    /* FIXME: should we move this call outside the transaction to keep the
     * transaction as short as possible? */
    status = ctx->ops.getpwnam_r(shortname_or_alias, pwd, buffer, buflen, &ret);
    ret = handle_getpw_result(status, pwd, dom, &del_user);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE,
              "getpwnam failed [%d]: %s\n", ret, strerror(ret));
        goto done;
    }

    if (del_user) {
        ret = delete_user(dom, i_name, 0);
        goto done;
    }

    uid = pwd->pw_uid;

    /* Canonicalize the username in case it was actually an alias */

    if (ctx->fast_alias == true) {
        ret = sysdb_getpwuid(tmpctx, dom, uid, &cached_pwd);
        if (ret != EOK) {
            /* Non-fatal, attempt to canonicalize online */
            DEBUG(SSSDBG_TRACE_FUNC, "Request to cache failed [%d]: %s\n",
                  ret, strerror(ret));
        }

        if (ret == EOK && cached_pwd->count == 1) {
            real_name = ldb_msg_find_attr_as_string(cached_pwd->msgs[0],
                                                    SYSDB_NAME, NULL);
            if (!real_name) {
                DEBUG(SSSDBG_MINOR_FAILURE, "Cached user has no name?\n");
            }
        }
    }

    if (real_name == NULL) {
        memset(buffer, 0, buflen);

        status = ctx->ops.getpwuid_r(uid, pwd, buffer, buflen, &ret);
        ret = handle_getpw_result(status, pwd, dom, &del_user);
        if (ret) {
            DEBUG(SSSDBG_OP_FAILURE,
                "getpwuid failed [%d]: %s\n", ret, strerror(ret));
            goto done;
        }

        real_name = sss_create_internal_fqname(tmpctx, pwd->pw_name, dom->name);
        if (real_name == NULL) {
            ret = ENOMEM;
            goto done;
        }
    }

    if (del_user) {
        ret = delete_user(dom, i_name, uid);
        goto done;
    }

    /* Both lookups went fine, we can save the user now */
    ret = save_user(dom, pwd, real_name, i_name);

done:
    talloc_zfree(tmpctx);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE,
              "proxy -> getpwnam_r failed for '%s' <%d>: %s\n",
               i_name, ret, strerror(ret));
    }
    return ret;
}

static int
handle_getpw_result(enum nss_status status, struct passwd *pwd,
                    struct sss_domain_info *dom, bool *del_user)
{
    int ret = EOK;

    if (!del_user) {
        return EINVAL;
    }
    *del_user = false;

    switch (status) {
    case NSS_STATUS_NOTFOUND:

        DEBUG(SSSDBG_TRACE_FUNC, "User not found.\n");
        *del_user = true;
        break;

    case NSS_STATUS_SUCCESS:

        DEBUG(SSSDBG_TRACE_FUNC, "User found: (%s, %"SPRIuid", %"SPRIgid")\n",
              pwd->pw_name, pwd->pw_uid, pwd->pw_gid);

        /* uid=0 or gid=0 are invalid values */
        /* also check that the id is in the valid range for this domain */
        if (OUT_OF_ID_RANGE(pwd->pw_uid, dom->id_min, dom->id_max) ||
            OUT_OF_ID_RANGE(pwd->pw_gid, dom->id_min, dom->id_max)) {

            DEBUG(SSSDBG_MINOR_FAILURE,
                  "User filtered out! (id out of range)\n");
            *del_user = true;
            break;
        }
        break;

    case NSS_STATUS_UNAVAIL:
        DEBUG(SSSDBG_MINOR_FAILURE,
              "Remote back end is not available. Entering offline mode\n");
        ret = ENXIO;
        break;

    default:
        DEBUG(SSSDBG_OP_FAILURE, "Unknown return code %d\n", status);
        ret = EIO;
        break;
    }

    return ret;
}

static int
delete_user(struct sss_domain_info *domain,
            const char *name, uid_t uid)
{
    int ret = EOK;

    if (name != NULL) {
        DEBUG(SSSDBG_TRACE_FUNC,
              "User %s does not exist (or is invalid) on remote server,"
              " deleting!\n", name);
    } else {
        DEBUG(SSSDBG_TRACE_FUNC,
              "User with UID %"SPRIuid" does not exist (or is invalid) "
              "on remote server, deleting!\n", uid);
    }

    ret = sysdb_delete_user(domain, name, uid);
    if (ret == ENOENT) {
        ret = EOK;
    }

    return ret;
}

static int
prepare_attrs_for_saving_ops(TALLOC_CTX *mem_ctx,
                             bool case_sensitive,
                             const char *real_name, /* already_qualified */
                             const char *alias, /* already qualified */
                             struct sysdb_attrs **attrs)
{
    const char *lc_name = NULL;
    const char *cased_alias = NULL;
    errno_t ret;

    if (!case_sensitive || alias != NULL) {
        if (*attrs == NULL) {
            *attrs = sysdb_new_attrs(mem_ctx);
            if (*attrs == NULL) {
                DEBUG(SSSDBG_CRIT_FAILURE, "Allocation error?!\n");
                ret = ENOMEM;
                goto done;
            }
        }
    }

    if (!case_sensitive) {
        lc_name = sss_tc_utf8_str_tolower(*attrs, real_name);
        if (lc_name == NULL) {
            DEBUG(SSSDBG_OP_FAILURE, "Cannot convert name to lowercase.\n");
            ret = ENOMEM;
            goto done;
        }

        ret = sysdb_attrs_add_string(*attrs, SYSDB_NAME_ALIAS, lc_name);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "Could not add name alias\n");
            ret = ENOMEM;
            goto done;
        }

    }

    if (alias != NULL) {
        cased_alias = sss_get_cased_name(*attrs, alias, case_sensitive);
        if (cased_alias == NULL) {
            ret = ENOMEM;
            goto done;
        }

        /* Add the alias only if it differs from lowercased pw_name */
        if (lc_name == NULL || strcmp(cased_alias, lc_name) != 0) {
            ret = sysdb_attrs_add_string(*attrs, SYSDB_NAME_ALIAS,
                                         cased_alias);
            if (ret != EOK) {
                DEBUG(SSSDBG_OP_FAILURE, "Could not add name alias\n");
                goto done;
            }
        }
    }

    ret = EOK;
done:
    return ret;
}

static int save_user(struct sss_domain_info *domain,
                     struct passwd *pwd,
                     const char *real_name, /* already qualified */
                     const char *alias) /* already qualified */
{
    const char *shell;
    const char *gecos;
    struct sysdb_attrs *attrs = NULL;
    errno_t ret;

    if (pwd->pw_shell && pwd->pw_shell[0] != '\0') {
        shell = pwd->pw_shell;
    } else {
        shell = NULL;
    }

    if (pwd->pw_gecos && pwd->pw_gecos[0] != '\0') {
        gecos = pwd->pw_gecos;
    } else {
        gecos = NULL;
    }

    ret = prepare_attrs_for_saving_ops(NULL, domain->case_sensitive,
                                       real_name, alias, &attrs);
    if (ret != EOK) {
        goto done;
    }

    ret = sysdb_store_user(domain,
                           real_name,
                           pwd->pw_passwd,
                           pwd->pw_uid,
                           pwd->pw_gid,
                           gecos,
                           pwd->pw_dir,
                           shell,
                           NULL,
                           attrs,
                           NULL,
                           domain->user_timeout,
                           0);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE, "Could not add user to cache\n");
        goto done;
    }

done:
    talloc_zfree(attrs);
    return ret;
}

/* =Getpwuid-wrapper======================================================*/

static int get_pw_uid(struct proxy_id_ctx *ctx,
                      struct sss_domain_info *dom,
                      uid_t uid)
{
    TALLOC_CTX *tmpctx;
    struct passwd *pwd;
    enum nss_status status;
    char *buffer;
    size_t buflen;
    bool del_user = false;
    int ret;
    char *name;

    DEBUG(SSSDBG_TRACE_FUNC, "Searching user by uid (%"SPRIuid")\n", uid);

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

    pwd = talloc_zero(tmpctx, struct passwd);
    if (!pwd) {
        ret = ENOMEM;
        goto done;
    }

    buflen = DEFAULT_BUFSIZE;
    buffer = talloc_size(tmpctx, buflen);
    if (!buffer) {
        ret = ENOMEM;
        goto done;
    }

    status = ctx->ops.getpwuid_r(uid, pwd, buffer, buflen, &ret);
    ret = handle_getpw_result(status, pwd, dom, &del_user);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE,
              "getpwuid failed [%d]: %s\n", ret, strerror(ret));
        goto done;
    }

    if (del_user) {
        ret = delete_user(dom, NULL, uid);
        goto done;
    }

    name = sss_create_internal_fqname(tmpctx, pwd->pw_name, dom->name);
    if (name == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "failed to qualify name '%s'\n",
              pwd->pw_name);
        goto done;
    }
    ret = save_user(dom, pwd, name, NULL);

done:
    talloc_zfree(tmpctx);
    if (ret) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "proxy -> getpwuid_r failed for '%"SPRIuid"' <%d>: %s\n",
               uid, ret, strerror(ret));
    }
    return ret;
}

/* =Getpwent-wrapper======================================================*/

static int enum_users(TALLOC_CTX *mem_ctx,
                      struct proxy_id_ctx *ctx,
                      struct sysdb_ctx *sysdb,
                      struct sss_domain_info *dom)
{
    TALLOC_CTX *tmpctx;
    bool in_transaction = false;
    struct passwd *pwd;
    enum nss_status status;
    size_t buflen;
    char *buffer;
    char *newbuf;
    int ret;
    errno_t sret;
    bool again;
    char *name;

    DEBUG(SSSDBG_TRACE_LIBS, "Enumerating users\n");

    tmpctx = talloc_new(mem_ctx);
    if (!tmpctx) {
        return ENOMEM;
    }

    pwd = talloc_zero(tmpctx, struct passwd);
    if (!pwd) {
        ret = ENOMEM;
        goto done;
    }

    buflen = DEFAULT_BUFSIZE;
    buffer = talloc_size(tmpctx, buflen);
    if (!buffer) {
        ret = ENOMEM;
        goto done;
    }

    ret = sysdb_transaction_start(sysdb);
    if (ret) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to start transaction\n");
        goto done;
    }
    in_transaction = true;

    status = ctx->ops.setpwent();
    if (status != NSS_STATUS_SUCCESS) {
        ret = EIO;
        goto done;
    }

    do {
        again = false;

        /* always zero out the pwd structure */
        memset(pwd, 0, sizeof(struct passwd));

        /* get entry */
        status = ctx->ops.getpwent_r(pwd, buffer, buflen, &ret);

        switch (status) {
            case NSS_STATUS_TRYAGAIN:
                /* buffer too small? */
                if (buflen < MAX_BUF_SIZE) {
                    buflen *= 2;
                }
                if (buflen > MAX_BUF_SIZE) {
                    buflen = MAX_BUF_SIZE;
                }
                newbuf = talloc_realloc_size(tmpctx, buffer, buflen);
                if (!newbuf) {
                    ret = ENOMEM;
                    goto done;
                }
                buffer = newbuf;
                again = true;
                break;

            case NSS_STATUS_NOTFOUND:

                /* we are done here */
                DEBUG(SSSDBG_TRACE_LIBS, "Enumeration completed.\n");

                ret = sysdb_transaction_commit(sysdb);
                if (ret != EOK) {
                    DEBUG(SSSDBG_CRIT_FAILURE, "Failed to commit transaction\n");
                    goto done;
                }
                in_transaction = false;
                break;

            case NSS_STATUS_SUCCESS:

                DEBUG(SSSDBG_TRACE_LIBS,
                      "User found (%s, %"SPRIuid", %"SPRIgid")\n",
                       pwd->pw_name, pwd->pw_uid, pwd->pw_gid);

                /* uid=0 or gid=0 are invalid values */
                /* also check that the id is in the valid range for this domain
                 */
                if (OUT_OF_ID_RANGE(pwd->pw_uid, dom->id_min, dom->id_max) ||
                    OUT_OF_ID_RANGE(pwd->pw_gid, dom->id_min, dom->id_max)) {

                    DEBUG(SSSDBG_OP_FAILURE, "User [%s] filtered out! (id out"
                        " of range)\n", pwd->pw_name);

                    again = true;
                    break;
                }

                name = sss_create_internal_fqname(tmpctx, pwd->pw_name, dom->name);
                if (name == NULL) {
                    DEBUG(SSSDBG_OP_FAILURE,
                          "failed to create internal name '%s'\n",
                          pwd->pw_name);
                    goto done;
                }
                ret = save_user(dom, pwd, name, NULL);
                if (ret) {
                    /* Do not fail completely on errors.
                     * Just report the failure to save and go on */
                    DEBUG(SSSDBG_OP_FAILURE, "Failed to store user %s."
                                " Ignoring.\n", pwd->pw_name);
                }
                again = true;
                break;

            case NSS_STATUS_UNAVAIL:
                /* "remote" backend unavailable. Enter offline mode */
                ret = ENXIO;
                break;

            default:
                ret = EIO;
                DEBUG(SSSDBG_OP_FAILURE, "proxy -> getpwent_r failed (%d)[%s]"
                            "\n", ret, strerror(ret));
                break;
        }
    } while (again);

done:
    talloc_zfree(tmpctx);
    if (in_transaction) {
        sret = sysdb_transaction_cancel(sysdb);
        if (sret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE, "Failed to cancel transaction\n");
        }
    }
    ctx->ops.endpwent();
    return ret;
}

/* =Save-group-utilities=================================================*/
#define DEBUG_GR_MEM(level, grp) \
    do { \
        if (!grp->gr_mem || !grp->gr_mem[0]) { \
            DEBUG(level, "Group %s has no members!\n", \
                          grp->gr_name); \
        } else { \
            int i = 0; \
            while (grp->gr_mem[i]) { \
                /* count */ \
                i++; \
            } \
            DEBUG(level, "Group %s has %d members!\n", \
                          grp->gr_name, i); \
        } \
    } while(0)


static errno_t remove_duplicate_group_members(TALLOC_CTX *mem_ctx,
                                             const struct group *orig_grp,
                                             struct group **_grp)
{
    TALLOC_CTX *tmp_ctx;
    hash_table_t *member_tbl = NULL;
    struct hash_iter_context_t *iter;
    hash_entry_t *entry;
    hash_key_t key;
    hash_value_t value;
    struct group *grp;
    size_t orig_member_count= 0;
    size_t member_count= 0;
    size_t i;
    errno_t ret;

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

    grp = talloc(tmp_ctx, struct group);
    if (grp == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "talloc failed.\n");
        ret = ENOMEM;
        goto done;
    }

    grp->gr_gid = orig_grp->gr_gid;

    grp->gr_name = talloc_strdup(grp, orig_grp->gr_name);
    if (grp->gr_name == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed.\n");
        ret = ENOMEM;
        goto done;
    }

    grp->gr_passwd = talloc_strdup(grp, orig_grp->gr_passwd);
    if (grp->gr_passwd == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed.\n");
        ret = ENOMEM;
        goto done;
    }

    if (orig_grp->gr_mem == NULL) {
        grp->gr_mem = NULL;
        ret = EOK;
        goto done;
    }

    for (i=0; orig_grp->gr_mem[i] != NULL; ++i) /* no-op: just counting */;

    orig_member_count = i;

    if (orig_member_count == 0) {
        grp->gr_mem = talloc_zero_array(grp, char *, 1);
        if (grp->gr_mem == NULL) {
            DEBUG(SSSDBG_OP_FAILURE, "talloc_zero_array failed.\n");
            ret = ENOMEM;
            goto done;
        }
        grp->gr_mem[0] = NULL;
        ret = EOK;
        goto done;
    }

    ret = sss_hash_create(tmp_ctx, orig_member_count, &member_tbl);
    if (ret != HASH_SUCCESS) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to create hash table.\n");
        ret = ENOMEM;
        goto done;
    }

    for (i=0; i < orig_member_count; ++i) {
        key.type = HASH_KEY_STRING;
        key.str = orig_grp->gr_mem[i]; /* hash_enter() makes copy itself */

        value.type = HASH_VALUE_PTR;
        /* no need to put copy in hash_table since
           copy will be created during construction of new grp */
        value.ptr = orig_grp->gr_mem[i];

        ret = hash_enter(member_tbl, &key, &value);
        if (ret != HASH_SUCCESS) {
            ret = ENOMEM;
            goto done;
        }
    }

    member_count = hash_count(member_tbl);
    if (member_count == 0) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Empty resulting hash table - must be internal bug.\n");
        ret = EINVAL;
        goto done;
    }

    grp->gr_mem = talloc_zero_array(grp, char *, member_count + 1);
    if (grp->gr_mem == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "talloc_zero_array failed.\n");
        ret = ENOMEM;
        goto done;
    }

    iter = new_hash_iter_context(member_tbl);
    if (iter == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "new_hash_iter_context failed.\n");
        ret = EINVAL;
        goto done;
    }

    i = 0;
    while ((entry = iter->next(iter)) != NULL) {
        grp->gr_mem[i] = talloc_strdup(grp, entry->key.str);
        if (grp->gr_mem[i] == NULL) {
            DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed.\n");
            ret = ENOMEM;
            goto done;
        }
        i++;
    }
    grp->gr_mem[i] = NULL;

    ret = EOK;

done:
    if (ret == EOK) {
        *_grp = talloc_steal(mem_ctx, grp);
    }
    talloc_zfree(tmp_ctx);

    return ret;
}

static errno_t proxy_process_missing_users(struct sysdb_ctx *sysdb,
                                           struct sss_domain_info *domain,
                                           struct sysdb_attrs *group_attrs,
                                           const char *const*fq_gr_mem,
                                           time_t now);
static int save_group(struct sysdb_ctx *sysdb, struct sss_domain_info *dom,
                      const struct group *grp,
                      const char *real_name, /* already qualified */
                      const char *alias) /* already qualified */
{
    errno_t ret, sret;
    struct group *ngroup = NULL;
    struct sysdb_attrs *attrs = NULL;
    TALLOC_CTX *tmp_ctx;
    time_t now = time(NULL);
    bool in_transaction = false;
    char **fq_gr_mem;

    tmp_ctx = talloc_new(NULL);
    if (!tmp_ctx) {
        return ENOMEM;
    }

    ret = remove_duplicate_group_members(tmp_ctx, grp, &ngroup);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to remove duplicate group members\n");
        goto done;
    }

    DEBUG_GR_MEM(SSSDBG_TRACE_LIBS, ngroup);

    ret = sysdb_transaction_start(sysdb);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to start transaction\n");
        goto done;
    }
    in_transaction = true;

    if (ngroup->gr_mem && ngroup->gr_mem[0]) {
        attrs = sysdb_new_attrs(tmp_ctx);
        if (!attrs) {
            DEBUG(SSSDBG_CRIT_FAILURE, "Allocation error?!\n");
            ret = ENOMEM;
            goto done;
        }

        fq_gr_mem = sss_create_internal_fqname_list(
                                            tmp_ctx,
                                            (const char *const*) ngroup->gr_mem,
                                            dom->name);
        if (fq_gr_mem == NULL) {
            ret = ENOMEM;
            goto done;
        }

        ret = sysdb_attrs_users_from_str_list(
                attrs, SYSDB_MEMBER, dom->name,
                (const char *const *) fq_gr_mem);
        if (ret) {
            DEBUG(SSSDBG_OP_FAILURE, "Could not add group members\n");
            goto done;
        }

        /* Create ghost users */
        ret = proxy_process_missing_users(sysdb, dom, attrs,
                                          (const char *const*) fq_gr_mem, now);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "Could not add missing members\n");
            goto done;
        }
    }

    ret = prepare_attrs_for_saving_ops(tmp_ctx, dom->case_sensitive,
                                       real_name, alias, &attrs);
    if (ret != EOK) {
        goto done;
    }

    ret = sysdb_store_group(dom,
                            real_name,
                            ngroup->gr_gid,
                            attrs,
                            dom->group_timeout,
                            now);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE, "Could not add group to cache\n");
        goto done;
    }

    ret = sysdb_transaction_commit(sysdb);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Could not commit transaction: [%s]\n",
               strerror(ret));
        goto done;
    }
    in_transaction = false;

done:
    if (in_transaction) {
        sret = sysdb_transaction_cancel(sysdb);
        if (sret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE, "Could not cancel transaction\n");
        }
    }
    talloc_free(tmp_ctx);
    return ret;
}

static errno_t proxy_process_missing_users(struct sysdb_ctx *sysdb,
                                           struct sss_domain_info *domain,
                                           struct sysdb_attrs *group_attrs,
                                           const char *const*fq_gr_mem,
                                           time_t now)
{
    errno_t ret;
    size_t i;
    TALLOC_CTX *tmp_ctx = NULL;
    struct ldb_message *msg;

    if (!sysdb || !fq_gr_mem) return EINVAL;

    tmp_ctx = talloc_new(NULL);
    if (!tmp_ctx) return ENOMEM;

    for (i = 0; fq_gr_mem[i]; i++) {
        ret = sysdb_search_user_by_name(tmp_ctx, domain, fq_gr_mem[i],
                                        NULL, &msg);
        if (ret == EOK) {
            /* Member already exists in the cache */
            DEBUG(SSSDBG_TRACE_INTERNAL,
                  "Member [%s] already cached\n", fq_gr_mem[i]);
            /* clean up */
            talloc_zfree(msg);
            continue;
        } else if (ret == ENOENT) {
            /* No entry for this user. Create a ghost user */
            DEBUG(SSSDBG_TRACE_LIBS,
                  "Member [%s] not cached, creating ghost user entry\n",
                   fq_gr_mem[i]);

            ret = sysdb_attrs_add_string(group_attrs, SYSDB_GHOST, fq_gr_mem[i]);
            if (ret != EOK) {
                DEBUG(SSSDBG_MINOR_FAILURE,
                      "Cannot store ghost user entry: [%d]: %s\n",
                       ret, strerror(ret));
                goto done;
            }
        } else {
            /* Unexpected error */
            DEBUG(SSSDBG_MINOR_FAILURE,
                  "Error searching cache for user [%s]: [%s]\n",
                   fq_gr_mem[i], strerror(ret));
            goto done;
        }
    }

    ret = EOK;
done:
    talloc_free(tmp_ctx);
    return ret;
}

/* =Getgrnam-wrapper======================================================*/
static char *
grow_group_buffer(TALLOC_CTX *mem_ctx,
                  char **buffer, size_t *buflen)
{
    char *newbuf;

    if (*buflen == 0) {
        *buflen = DEFAULT_BUFSIZE;
    }
    if (*buflen < MAX_BUF_SIZE) {
        *buflen *= 2;
    }
    if (*buflen > MAX_BUF_SIZE) {
        *buflen = MAX_BUF_SIZE;
    }

    newbuf = talloc_realloc_size(mem_ctx, *buffer, *buflen);
    if (!newbuf) {
        return NULL;
    }
    *buffer = newbuf;

    return *buffer;
}

static errno_t
handle_getgr_result(enum nss_status status, struct group *grp,
                    struct sss_domain_info *dom,
                    bool *delete_group)
{
    if (delete_group) {
        *delete_group = false;
    }

    switch (status) {
    case NSS_STATUS_TRYAGAIN:
        DEBUG(SSSDBG_MINOR_FAILURE, "Buffer too small\n");
        return EAGAIN;

    case NSS_STATUS_NOTFOUND:
        DEBUG(SSSDBG_MINOR_FAILURE, "Group not found.\n");
        if (delete_group) {
            *delete_group = true;
        }
        break;

    case NSS_STATUS_SUCCESS:
        DEBUG(SSSDBG_FUNC_DATA, "Group found: (%s, %"SPRIgid")\n",
              grp->gr_name, grp->gr_gid);

        /* gid=0 is an invalid value */
        /* also check that the id is in the valid range for this domain */
        if (OUT_OF_ID_RANGE(grp->gr_gid, dom->id_min, dom->id_max)) {
            DEBUG(SSSDBG_MINOR_FAILURE,
                  "Group filtered out! (id out of range)\n");
            if (delete_group) {
                *delete_group = true;
            }
            break;
        }
        break;

    case NSS_STATUS_UNAVAIL:
        DEBUG(SSSDBG_MINOR_FAILURE,
              "Remote back end is not available. Entering offline mode\n");
        return ENXIO;

    default:
        DEBUG(SSSDBG_OP_FAILURE, "Unknown return code %d\n", status);
        return EIO;
    }

    return EOK;
}

static int get_gr_name(struct proxy_id_ctx *ctx,
                       struct sysdb_ctx *sysdb,
                       struct sss_domain_info *dom,
                       const char *i_name)
{
    TALLOC_CTX *tmpctx;
    struct group *grp;
    enum nss_status status;
    char *buffer = 0;
    size_t buflen = 0;
    bool delete_group = false;
    int ret;
    gid_t gid;
    struct ldb_result *cached_grp = NULL;
    const char *real_name = NULL;
    char *shortname_or_alias;

    DEBUG(SSSDBG_FUNC_DATA, "Searching group by name (%s)\n", i_name);

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

    ret = sss_parse_internal_fqname(tmpctx, i_name, &shortname_or_alias, NULL);
    if (ret != EOK) {
        goto done;
    }

    grp = talloc(tmpctx, struct group);
    if (!grp) {
        ret = ENOMEM;
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc() failed\n");
        goto done;
    }

    do {
        /* always zero out the grp structure */
        memset(grp, 0, sizeof(struct group));
        buffer = grow_group_buffer(tmpctx, &buffer, &buflen);
        if (!buffer) {
            ret = ENOMEM;
            goto done;
        }

        status = ctx->ops.getgrnam_r(shortname_or_alias, grp, buffer,
                                     buflen, &ret);
        ret = handle_getgr_result(status, grp, dom, &delete_group);
    } while (ret == EAGAIN);

    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "getgrnam failed [%d]: %s\n", ret, strerror(ret));
        goto done;
    }

    if (delete_group) {
        DEBUG(SSSDBG_TRACE_FUNC,
              "Group %s does not exist (or is invalid) on remote server,"
               " deleting!\n", i_name);

        ret = sysdb_delete_group(dom, i_name, 0);
        if (ret == ENOENT) {
            ret = EOK;
        }
        goto done;
    }

    gid = grp->gr_gid;

    /* Canonicalize the group name in case it was actually an alias */
    if (ctx->fast_alias == true) {
        ret = sysdb_getgrgid(tmpctx, dom, gid, &cached_grp);
        if (ret != EOK) {
            /* Non-fatal, attempt to canonicalize online */
            DEBUG(SSSDBG_TRACE_FUNC, "Request to cache failed [%d]: %s\n",
                  ret, strerror(ret));
        }

        if (ret == EOK && cached_grp->count == 1) {
            real_name = ldb_msg_find_attr_as_string(cached_grp->msgs[0],
                                                    SYSDB_NAME, NULL);
            if (!real_name) {
                DEBUG(SSSDBG_MINOR_FAILURE, "Cached group has no name?\n");
            }
        }
    }

    if (real_name == NULL) {
        talloc_zfree(buffer);
        buflen = 0;

        do {
            memset(grp, 0, sizeof(struct group));
            buffer = grow_group_buffer(tmpctx, &buffer, &buflen);
            if (!buffer) {
                ret = ENOMEM;
                goto done;
            }

            status = ctx->ops.getgrgid_r(gid, grp, buffer, buflen, &ret);

            ret = handle_getgr_result(status, grp, dom, &delete_group);
        } while (ret == EAGAIN);

        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE,
                "getgrgid failed [%d]: %s\n", ret, strerror(ret));
            goto done;
        }

        real_name = sss_create_internal_fqname(tmpctx, grp->gr_name, dom->name);
        if (real_name == NULL) {
            DEBUG(SSSDBG_OP_FAILURE, "Failed to create fqdn '%s'\n",
                  grp->gr_name);
            ret = ENOMEM;
            goto done;
        }
    }

    if (delete_group) {
        DEBUG(SSSDBG_TRACE_FUNC,
              "Group %s does not exist (or is invalid) on remote server,"
               " deleting!\n", i_name);

        ret = sysdb_delete_group(dom, i_name, gid);
        if (ret == ENOENT) {
            ret = EOK;
        }
        goto done;
    }

    ret = save_group(sysdb, dom, grp, real_name, i_name);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Cannot save group [%d]: %s\n", ret, strerror(ret));
        goto done;
    }

done:
    talloc_zfree(tmpctx);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE,
              "proxy -> getgrnam_r failed for '%s' <%d>: %s\n",
              i_name, ret, strerror(ret));
    }
    return ret;
}

/* =Getgrgid-wrapper======================================================*/
static int get_gr_gid(TALLOC_CTX *mem_ctx,
                      struct proxy_id_ctx *ctx,
                      struct sysdb_ctx *sysdb,
                      struct sss_domain_info *dom,
                      gid_t gid,
                      time_t now)
{
    TALLOC_CTX *tmpctx;
    struct group *grp;
    enum nss_status status;
    char *buffer = NULL;
    size_t buflen = 0;
    bool delete_group = false;
    int ret;
    char *name;

    DEBUG(SSSDBG_TRACE_FUNC, "Searching group by gid (%"SPRIgid")\n", gid);

    tmpctx = talloc_new(mem_ctx);
    if (!tmpctx) {
        return ENOMEM;
    }

    grp = talloc(tmpctx, struct group);
    if (!grp) {
        ret = ENOMEM;
        goto done;
    }

    do {
        /* always zero out the grp structure */
        memset(grp, 0, sizeof(struct group));
        buffer = grow_group_buffer(tmpctx, &buffer, &buflen);
        if (!buffer) {
            ret = ENOMEM;
            goto done;
        }

        status = ctx->ops.getgrgid_r(gid, grp, buffer, buflen, &ret);

        ret = handle_getgr_result(status, grp, dom, &delete_group);
    } while (ret == EAGAIN);

    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "getgrgid failed [%d]: %s\n", ret, strerror(ret));
        goto done;
    }

    if (delete_group) {
        DEBUG(SSSDBG_TRACE_FUNC,
              "Group %"SPRIgid" does not exist (or is invalid) on remote "
               "server, deleting!\n", gid);

        ret = sysdb_delete_group(dom, NULL, gid);
        if (ret == ENOENT) {
            ret = EOK;
        }
        goto done;
    }

    name = sss_create_internal_fqname(tmpctx, grp->gr_name, dom->name);
    if (name == NULL) {
        ret = ENOMEM;
        goto done;
    }

    ret = save_group(sysdb, dom, grp, name, NULL);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Cannot save user [%d]: %s\n", ret, strerror(ret));
        goto done;
    }

done:
    talloc_zfree(tmpctx);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE,
              "proxy -> getgrgid_r failed for '%"SPRIgid"' <%d>: %s\n",
               gid, ret, strerror(ret));
    }
    return ret;
}

/* =Getgrent-wrapper======================================================*/

static int enum_groups(TALLOC_CTX *mem_ctx,
                       struct proxy_id_ctx *ctx,
                       struct sysdb_ctx *sysdb,
                       struct sss_domain_info *dom)
{
    TALLOC_CTX *tmpctx;
    bool in_transaction = false;
    struct group *grp;
    enum nss_status status;
    size_t buflen;
    char *buffer;
    char *newbuf;
    int ret;
    errno_t sret;
    bool again;
    char *name;

    DEBUG(SSSDBG_TRACE_LIBS, "Enumerating groups\n");

    tmpctx = talloc_new(mem_ctx);
    if (!tmpctx) {
        return ENOMEM;
    }

    grp = talloc(tmpctx, struct group);
    if (!grp) {
        ret = ENOMEM;
        goto done;
    }

    buflen = DEFAULT_BUFSIZE;
    buffer = talloc_size(tmpctx, buflen);
    if (!buffer) {
        ret = ENOMEM;
        goto done;
    }

    ret = sysdb_transaction_start(sysdb);
    if (ret) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to start transaction\n");
        goto done;
    }
    in_transaction = true;

    status = ctx->ops.setgrent();
    if (status != NSS_STATUS_SUCCESS) {
        ret = EIO;
        goto done;
    }

    do {
        again = false;

        /* always zero out the grp structure */
        memset(grp, 0, sizeof(struct group));

        /* get entry */
        status = ctx->ops.getgrent_r(grp, buffer, buflen, &ret);

        switch (status) {
            case NSS_STATUS_TRYAGAIN:
                /* buffer too small? */
                if (buflen < MAX_BUF_SIZE) {
                    buflen *= 2;
                }
                if (buflen > MAX_BUF_SIZE) {
                    buflen = MAX_BUF_SIZE;
                }
                newbuf = talloc_realloc_size(tmpctx, buffer, buflen);
                if (!newbuf) {
                    ret = ENOMEM;
                    goto done;
                }
                buffer = newbuf;
                again = true;
                break;

            case NSS_STATUS_NOTFOUND:

                /* we are done here */
                DEBUG(SSSDBG_TRACE_LIBS, "Enumeration completed.\n");

                ret = sysdb_transaction_commit(sysdb);
                if (ret != EOK) {
                    DEBUG(SSSDBG_CRIT_FAILURE, "Failed to commit transaction\n");
                    goto done;
                }
                in_transaction = false;
                break;

            case NSS_STATUS_SUCCESS:

                DEBUG(SSSDBG_OP_FAILURE, "Group found (%s, %"SPRIgid")\n",
                            grp->gr_name, grp->gr_gid);

                /* gid=0 is an invalid value */
                /* also check that the id is in the valid range for this domain
                 */
                if (OUT_OF_ID_RANGE(grp->gr_gid, dom->id_min, dom->id_max)) {

                    DEBUG(SSSDBG_OP_FAILURE, "Group [%s] filtered out! (id"
                        "out of range)\n", grp->gr_name);

                    again = true;
                    break;
                }

                name = sss_create_internal_fqname(tmpctx, grp->gr_name,
                                                  dom->name);
                if (name == NULL) {
                    DEBUG(SSSDBG_OP_FAILURE, "Failed to create internal fqname "
                          "Ignoring\n");
                    ret = ENOMEM;
                }
                ret = save_group(sysdb, dom, grp, name, NULL);
                if (ret) {
                    /* Do not fail completely on errors.
                     * Just report the failure to save and go on */
                    DEBUG(SSSDBG_OP_FAILURE, "Failed to store group."
                                "Ignoring\n");
                }
                again = true;
                break;

            case NSS_STATUS_UNAVAIL:
                /* "remote" backend unavailable. Enter offline mode */
                ret = ENXIO;
                break;

            default:
                ret = EIO;
                DEBUG(SSSDBG_OP_FAILURE, "proxy -> getgrent_r failed (%d)[%s]"
                            "\n", ret, strerror(ret));
                break;
        }
    } while (again);

done:
    talloc_zfree(tmpctx);
    if (in_transaction) {
        sret = sysdb_transaction_cancel(sysdb);
        if (sret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE, "Failed to cancel transaction\n");
        }
    }
    ctx->ops.endgrent();
    return ret;
}


/* =Initgroups-wrapper====================================================*/

static int get_initgr_groups_process(TALLOC_CTX *memctx,
                                     struct proxy_id_ctx *ctx,
                                     struct sysdb_ctx *sysdb,
                                     struct sss_domain_info *dom,
                                     struct passwd *pwd);

static int get_initgr(TALLOC_CTX *mem_ctx,
                      struct proxy_id_ctx *ctx,
                      struct sysdb_ctx *sysdb,
                      struct sss_domain_info *dom,
                      const char *i_name)
{
    TALLOC_CTX *tmpctx;
    bool in_transaction = false;
    struct passwd *pwd;
    enum nss_status status;
    char *buffer;
    size_t buflen;
    int ret;
    errno_t sret;
    bool del_user;
    uid_t uid;
    struct ldb_result *cached_pwd = NULL;
    const char *real_name = NULL;
    char *shortname_or_alias;

    tmpctx = talloc_new(mem_ctx);
    if (!tmpctx) {
        return ENOMEM;
    }

    ret = sss_parse_internal_fqname(tmpctx, i_name, &shortname_or_alias, NULL);
    if (ret != EOK) {
        goto done;
    }

    pwd = talloc_zero(tmpctx, struct passwd);
    if (!pwd) {
        ret = ENOMEM;
        goto fail;
    }

    buflen = DEFAULT_BUFSIZE;
    buffer = talloc_size(tmpctx, buflen);
    if (!buffer) {
        ret = ENOMEM;
        goto fail;
    }

    ret = sysdb_transaction_start(sysdb);
    if (ret) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to start transaction\n");
        goto fail;
    }
    in_transaction = true;

    /* FIXME: should we move this call outside the transaction to keep the
     * transaction as short as possible? */
    status = ctx->ops.getpwnam_r(shortname_or_alias, pwd,
                                 buffer, buflen, &ret);
    ret = handle_getpw_result(status, pwd, dom, &del_user);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE,
              "getpwnam failed [%d]: %s\n", ret, strerror(ret));
        goto fail;
    }

    if (del_user) {
        ret = delete_user(dom, i_name, 0);
        if (ret) {
            DEBUG(SSSDBG_OP_FAILURE, "Could not delete user\n");
            goto fail;
        }
        goto done;
    }

    uid = pwd->pw_uid;

    /* Canonicalize the username in case it was actually an alias */
    if (ctx->fast_alias == true) {
        ret = sysdb_getpwuid(tmpctx, dom, uid, &cached_pwd);
        if (ret != EOK) {
            /* Non-fatal, attempt to canonicalize online */
            DEBUG(SSSDBG_TRACE_FUNC, "Request to cache failed [%d]: %s\n",
                  ret, strerror(ret));
        }

        if (ret == EOK && cached_pwd->count == 1) {
            real_name = ldb_msg_find_attr_as_string(cached_pwd->msgs[0],
                                                    SYSDB_NAME, NULL);
            if (!real_name) {
                DEBUG(SSSDBG_MINOR_FAILURE, "Cached user has no name?\n");
            }
        }
    }

    if (real_name == NULL) {
        memset(buffer, 0, buflen);

        status = ctx->ops.getpwuid_r(uid, pwd, buffer, buflen, &ret);
        ret = handle_getpw_result(status, pwd, dom, &del_user);
        if (ret) {
            DEBUG(SSSDBG_OP_FAILURE,
                "getpwuid failed [%d]: %s\n", ret, strerror(ret));
            goto done;
        }

        real_name = sss_create_internal_fqname(tmpctx, pwd->pw_name, dom->name);
        if (real_name == NULL) {
            ret = ENOMEM;
            goto done;
        }
    }

    if (del_user) {
        ret = delete_user(dom, i_name, uid);
        if (ret) {
            DEBUG(SSSDBG_OP_FAILURE, "Could not delete user\n");
            goto fail;
        }
        goto done;
    }

    ret = save_user(dom, pwd, real_name, i_name);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE, "Could not save user\n");
        goto fail;
    }

    ret = get_initgr_groups_process(tmpctx, ctx, sysdb, dom, pwd);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Could not process initgroups\n");
        goto fail;
    }

done:
    ret = sysdb_transaction_commit(sysdb);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE, "Failed to commit transaction\n");
        goto fail;
    }
    in_transaction = false;

fail:
    talloc_zfree(tmpctx);
    if (in_transaction) {
        sret = sysdb_transaction_cancel(sysdb);
        if (sret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE, "Failed to cancel transaction\n");
        }
    }
    return ret;
}

static int remove_group_members(struct proxy_id_ctx *ctx,
                                struct sss_domain_info *dom,
                                const struct passwd *pwd,
                                long int num_gids,
                                const gid_t *gids,
                                long int num_cached_gids,
                                const gid_t *cached_gids)
{
    TALLOC_CTX *tmp_ctx = NULL;
    int i = 0, j = 0;
    int ret = EOK;
    const char *groupname = NULL;
    const char *username = NULL;
    bool group_found = false;
    struct ldb_result *res = NULL;

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

    username = sss_create_internal_fqname(tmp_ctx, pwd->pw_name, dom->name);
    if (username == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "Failed to create fqdn '%s'\n", pwd->pw_name);
        ret = ENOMEM;
        goto done;
    }

    for (i = 0; i < num_cached_gids; i++) {
        group_found = false;
        /* group 0 is the primary group so it can be skipped */
        for (j = 1; j < num_gids; j++) {
            if (cached_gids[i] == gids[j]) {
                group_found = true;
                break;
            }
        }

        if (!group_found) {
            ret = sysdb_getgrgid(tmp_ctx, dom, cached_gids[i], &res);
            if (ret != EOK || res->count != 1) {
                DEBUG(SSSDBG_OP_FAILURE,
                      "sysdb_getgrgid failed for GID [%d].\n", cached_gids[i]);
                continue;
            }

            groupname = ldb_msg_find_attr_as_string(res->msgs[0], SYSDB_NAME, NULL);
            if (groupname == NULL) {
                DEBUG(SSSDBG_OP_FAILURE,
                      "Attribute is missing but this should never happen!\n");
                continue;
            }

            ret = sysdb_remove_group_member(dom, groupname,
                                            username,
                                            SYSDB_MEMBER_USER, false);
            if (ret != EOK) {
                DEBUG(SSSDBG_CRIT_FAILURE,
                      "Could not remove member [%s] from group [%s]\n",
                      username, groupname);
                continue;
            }
        }
    }

    ret = EOK;

done:
    talloc_free(tmp_ctx);
    return ret;
}

static int get_cached_user_groups(struct sysdb_ctx *sysdb,
                                  struct sss_domain_info *dom,
                                  const struct passwd *pwd,
                                  unsigned int *_num_cached_gids,
                                  gid_t **_cached_gids)
{
    TALLOC_CTX *tmp_ctx = NULL;
    int ret = EOK;
    int i = 0, j = 0;
    gid_t gid = 0;
    gid_t *cached_gids = NULL;
    const char *username = NULL;
    struct ldb_result *res = NULL;

    if (_num_cached_gids == NULL || _cached_gids == NULL) {
        return EINVAL;
    }

    tmp_ctx = talloc_new(NULL);
    if (!tmp_ctx) {
        ret = ENOMEM;
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc_new() failed\n");
        goto done;
    }

    username = sss_create_internal_fqname(tmp_ctx, pwd->pw_name, dom->name);
    if (username == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "Failed to create fqdn '%s'\n", pwd->pw_name);
        ret = ENOMEM;
        goto done;
    }

    ret = sysdb_initgroups(tmp_ctx, dom, username, &res);
    /* the first element is the user itself so it can be skipped */
    if (ret == EOK && res->count > 1) {
        cached_gids = talloc_array(tmp_ctx, gid_t, res->count - 1);

        for (i = 1; i < res->count; i++) {
            gid = ldb_msg_find_attr_as_uint(res->msgs[i], SYSDB_GIDNUM, 0);
            if (gid != 0) {
                cached_gids[j] = gid;
                j++;
            }
        }

        *_num_cached_gids = j;
        *_cached_gids = talloc_steal(sysdb, cached_gids);
    } else if (ret == EOK) {
        *_num_cached_gids = 0;
        *_cached_gids = NULL;
    } else {
        goto done;
    }

    ret = EOK;

done:
    talloc_zfree(tmp_ctx);

    return ret;
}

static int get_initgr_groups_process(TALLOC_CTX *memctx,
                                     struct proxy_id_ctx *ctx,
                                     struct sysdb_ctx *sysdb,
                                     struct sss_domain_info *dom,
                                     struct passwd *pwd)
{
    enum nss_status status;
    long int limit;
    long int size;
    long int num;
    long int num_gids;
    gid_t *gids;
    int ret;
    int i;
    time_t now;
    gid_t *cached_gids = NULL;
    unsigned int num_cached_gids = 0;

    num_gids = 0;
    limit = 4096;
    num = 4096;
    size = num*sizeof(gid_t);
    gids = talloc_size(memctx, size);
    if (!gids) {
        return ENOMEM;
    }

    /* nss modules may skip the primary group when we pass it in so always add
     * it in advance */
    gids[0] = pwd->pw_gid;
    num_gids++;

    /* FIXME: should we move this call outside the transaction to keep the
     * transaction as short as possible? */
    do {
        status = ctx->ops.initgroups_dyn(pwd->pw_name, pwd->pw_gid, &num_gids,
                &num, &gids, limit, &ret);

        if (status == NSS_STATUS_TRYAGAIN) {
            /* buffer too small? */
            if (size < MAX_BUF_SIZE) {
                num *= 2;
                size = num*sizeof(gid_t);
            }
            if (size > MAX_BUF_SIZE) {
                size = MAX_BUF_SIZE;
                num = size/sizeof(gid_t);
            }
            limit = num;
            gids = talloc_realloc_size(memctx, gids, size);
            if (!gids) {
                return ENOMEM;
            }
        }
    } while(status == NSS_STATUS_TRYAGAIN);

    switch (status) {
    case NSS_STATUS_NOTFOUND:
        DEBUG(SSSDBG_FUNC_DATA, "The initgroups call returned 'NOTFOUND'. "
                                 "Assume the user is only member of its "
                                 "primary group (%"SPRIgid")\n", pwd->pw_gid);
        /* fall through */
        SSS_ATTRIBUTE_FALLTHROUGH;
    case NSS_STATUS_SUCCESS:
        DEBUG(SSSDBG_CONF_SETTINGS, "User [%s] appears to be member of %lu "
              "groups\n", pwd->pw_name, num_gids);

        ret = get_cached_user_groups(sysdb, dom, pwd, &num_cached_gids, &cached_gids);
        if (ret) {
            return ret;
        }
        ret = remove_group_members(ctx, dom, pwd, num_gids, gids, num_cached_gids, cached_gids);
        talloc_free(cached_gids);
        if (ret) {
            return ret;
        }

        now = time(NULL);
        for (i = 0; i < num_gids; i++) {
            ret = get_gr_gid(memctx, ctx, sysdb, dom, gids[i], now);
            if (ret) {
                return ret;
            }
        }
        ret = EOK;

        break;

    default:
        DEBUG(SSSDBG_OP_FAILURE, "proxy -> initgroups_dyn failed (%d)[%s]\n",
                  ret, strerror(ret));
        ret = EIO;
        break;
    }

    return ret;
}

/* =Proxy_Id-Functions====================================================*/

static struct dp_reply_std
proxy_account_info(TALLOC_CTX *mem_ctx,
                   struct proxy_id_ctx *ctx,
                   struct dp_id_data *data,
                   struct be_ctx *be_ctx,
                   struct sss_domain_info *domain)
{
    struct dp_reply_std reply;
    struct sysdb_ctx *sysdb;
    uid_t uid;
    gid_t gid;
    errno_t ret;
    char *endptr;

    sysdb = domain->sysdb;

    /* Proxy provider does not support security ID lookups. */
    if (data->filter_type == BE_FILTER_SECID) {
        dp_reply_std_set(&reply, DP_ERR_FATAL, ENOSYS,
                         "Security lookups are not supported");
        return reply;
    }

    switch (data->entry_type & BE_REQ_TYPE_MASK) {
    case BE_REQ_USER: /* user */
        switch (data->filter_type) {
        case BE_FILTER_ENUM:
            ret = enum_users(mem_ctx, ctx, sysdb, domain);
            break;

        case BE_FILTER_NAME:
            ret = get_pw_name(ctx, domain, data->filter_value);
            break;

        case BE_FILTER_IDNUM:
            uid = (uid_t) strtouint32(data->filter_value, &endptr, 10);
            if (errno || *endptr || (data->filter_value == endptr)) {
                dp_reply_std_set(&reply, DP_ERR_FATAL, EINVAL,
                                 "Invalid attr type");
                return reply;
            }
            ret = get_pw_uid(ctx, domain, uid);
            break;
        default:
            dp_reply_std_set(&reply, DP_ERR_FATAL, EINVAL,
                             "Invalid filter type");
            return reply;
        }
        break;

    case BE_REQ_GROUP: /* group */
        switch (data->filter_type) {
        case BE_FILTER_ENUM:
            ret = enum_groups(mem_ctx, ctx, sysdb, domain);
            break;
        case BE_FILTER_NAME:
            ret = get_gr_name(ctx, sysdb, domain, data->filter_value);
            break;
        case BE_FILTER_IDNUM:
            gid = (gid_t) strtouint32(data->filter_value, &endptr, 10);
            if (errno || *endptr || (data->filter_value == endptr)) {
                dp_reply_std_set(&reply, DP_ERR_FATAL, EINVAL,
                                 "Invalid attr type");
                return reply;
            }
            ret = get_gr_gid(mem_ctx, ctx, sysdb, domain, gid, 0);
            break;
        default:
            dp_reply_std_set(&reply, DP_ERR_FATAL, EINVAL,
                             "Invalid filter type");
            return reply;
        }
        break;

    case BE_REQ_INITGROUPS: /* init groups for user */
        if (data->filter_type != BE_FILTER_NAME) {
            dp_reply_std_set(&reply, DP_ERR_FATAL, EINVAL,
                             "Invalid filter type");
            return reply;
        }
        if (ctx->ops.initgroups_dyn == NULL) {
            dp_reply_std_set(&reply, DP_ERR_FATAL, ENODEV,
                             "Initgroups call not supported");
            return reply;
        }
        ret = get_initgr(mem_ctx, ctx, sysdb, domain, data->filter_value);
        break;

    case BE_REQ_NETGROUP:
        if (data->filter_type != BE_FILTER_NAME) {
            dp_reply_std_set(&reply, DP_ERR_FATAL, EINVAL,
                             "Invalid filter type");
            return reply;
        }
        if (ctx->ops.setnetgrent == NULL || ctx->ops.getnetgrent_r == NULL ||
            ctx->ops.endnetgrent == NULL) {
            dp_reply_std_set(&reply, DP_ERR_FATAL, ENODEV,
                             "Netgroups are not supported");
            return reply;
        }

        ret = get_netgroup(ctx, domain, data->filter_value);
        break;

    case BE_REQ_SERVICES:
        switch (data->filter_type) {
        case BE_FILTER_NAME:
            if (ctx->ops.getservbyname_r == NULL) {
                dp_reply_std_set(&reply, DP_ERR_FATAL, ENODEV,
                                 "Services are not supported");
                return reply;
            }
            ret = get_serv_byname(ctx, domain,
                                  data->filter_value,
                                  data->extra_value);
            break;
        case BE_FILTER_IDNUM:
            if (ctx->ops.getservbyport_r == NULL) {
                dp_reply_std_set(&reply, DP_ERR_FATAL, ENODEV,
                                 "Services are not supported");
                return reply;
            }
            ret = get_serv_byport(ctx, domain,
                                  data->filter_value,
                                  data->extra_value);
            break;
        case BE_FILTER_ENUM:
            if (!ctx->ops.setservent
                    || !ctx->ops.getservent_r
                    || !ctx->ops.endservent) {
                dp_reply_std_set(&reply, DP_ERR_FATAL, ENODEV,
                                 "Services are not supported");
                return reply;
            }
            ret = enum_services(ctx, sysdb, domain);
            break;
        default:
            dp_reply_std_set(&reply, DP_ERR_FATAL, EINVAL,
                             "Invalid filter type");
            return reply;
        }
        break;

    case BE_REQ_BY_CERT:
        if (data->filter_type != BE_FILTER_CERT) {
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "Unexpected filter type for lookup by cert: %d\n",
                  data->filter_type);
            dp_reply_std_set(&reply, DP_ERR_FATAL, EINVAL,
                             "Unexpected filter type for lookup by cert");
            return reply;
        }

        if (ctx->sss_certmap_ctx == NULL) {
            DEBUG(SSSDBG_TRACE_ALL, "Certificate mapping not configured.\n");
            ret = EOK;
            break;
        }

        ret = proxy_map_cert_to_user(ctx, data);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "proxy_map_cert_to_user failed\n");
        }
        break;

    default: /*fail*/
        dp_reply_std_set(&reply, DP_ERR_FATAL, EINVAL,
                         "Invalid filter type");
        return reply;
    }

    if (ret) {
        if (ret == ENXIO) {
            DEBUG(SSSDBG_OP_FAILURE,
                  "proxy returned UNAVAIL error, going offline!\n");
            be_mark_offline(be_ctx);
        }

        dp_reply_std_set(&reply, DP_ERR_FATAL, ret, NULL);
        return reply;
    }

    dp_reply_std_set(&reply, DP_ERR_OK, EOK, NULL);
    return reply;
}

struct proxy_account_info_handler_state {
    struct dp_reply_std reply;
};

struct tevent_req *
proxy_account_info_handler_send(TALLOC_CTX *mem_ctx,
                               struct proxy_id_ctx *id_ctx,
                               struct dp_id_data *data,
                               struct dp_req_params *params)
{
    struct proxy_account_info_handler_state *state;
    struct tevent_req *req;

    req = tevent_req_create(mem_ctx, &state,
                            struct proxy_account_info_handler_state);
    if (req == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n");
        return NULL;
    }

    state->reply = proxy_account_info(state, id_ctx, data, params->be_ctx,
                                      params->be_ctx->domain);

    /* TODO For backward compatibility we always return EOK to DP now. */
    tevent_req_done(req);
    tevent_req_post(req, params->ev);

    return req;
}

errno_t proxy_account_info_handler_recv(TALLOC_CTX *mem_ctx,
                                       struct tevent_req *req,
                                       struct dp_reply_std *data)
{
    struct proxy_account_info_handler_state *state = NULL;

    state = tevent_req_data(req, struct proxy_account_info_handler_state);

    TEVENT_REQ_RETURN_ON_ERROR(req);

    *data = state->reply;

    return EOK;
}
