/*
   SSSD

   Proxy Module

   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 <nss.h>
#include <errno.h>
#include <pwd.h>
#include <grp.h>
#include <dlfcn.h>

#include <security/pam_appl.h>
#include <security/pam_modules.h>

#include "util/util.h"
#include "providers/dp_backend.h"
#include "db/sysdb.h"
#include "../sss_client/sss_cli.h"

struct proxy_nss_ops {
    enum nss_status (*getpwnam_r)(const char *name, struct passwd *result,
                                  char *buffer, size_t buflen, int *errnop);
    enum nss_status (*getpwuid_r)(uid_t uid, struct passwd *result,
                                  char *buffer, size_t buflen, int *errnop);
    enum nss_status (*setpwent)(void);
    enum nss_status (*getpwent_r)(struct passwd *result,
                                  char *buffer, size_t buflen, int *errnop);
    enum nss_status (*endpwent)(void);

    enum nss_status (*getgrnam_r)(const char *name, struct group *result,
                                  char *buffer, size_t buflen, int *errnop);
    enum nss_status (*getgrgid_r)(gid_t gid, struct group *result,
                                  char *buffer, size_t buflen, int *errnop);
    enum nss_status (*setgrent)(void);
    enum nss_status (*getgrent_r)(struct group *result,
                                  char *buffer, size_t buflen, int *errnop);
    enum nss_status (*endgrent)(void);
    enum nss_status (*initgroups_dyn)(const char *user, gid_t group,
                                      long int *start, long int *size,
                                      gid_t **groups, long int limit,
                                      int *errnop);
};

struct proxy_ctx {
    struct proxy_nss_ops ops;
};

struct authtok_conv {
    char *authtok;
    char *oldauthtok;
};

static int proxy_internal_conv(int num_msg, const struct pam_message **msgm,
                            struct pam_response **response,
                            void *appdata_ptr) {
    int i;
    struct pam_response *reply;
    struct authtok_conv *auth_data;

    auth_data = talloc_get_type(appdata_ptr, struct authtok_conv);

    if (num_msg <= 0) return PAM_CONV_ERR;

    reply = (struct pam_response *) calloc(num_msg,
                                           sizeof(struct pam_response));
    if (reply == NULL) return PAM_CONV_ERR;

    for (i=0; i < num_msg; i++) {
        switch( msgm[i]->msg_style ) {
            case PAM_PROMPT_ECHO_OFF:
                DEBUG(4, ("Conversation message: %s.\n", msgm[i]->msg));
                reply[i].resp_retcode = 0;
                reply[i].resp = strdup(auth_data->authtok);
                break;
            default:
                DEBUG(1, ("Conversation style %d not supported.\n",
                           msgm[i]->msg_style));
                goto failed;
        }
    }

    *response = reply;
    reply = NULL;

    return PAM_SUCCESS;

failed:
    free(reply);
    return PAM_CONV_ERR;
}

static void proxy_pam_handler(struct be_req *req) {
    int ret;
    int pam_status;
    pam_handle_t *pamh=NULL;
    struct authtok_conv *auth_data;
    struct pam_conv conv;
    struct pam_data *pd;

    pd = talloc_get_type(req->req_data, struct pam_data);

    conv.conv=proxy_internal_conv;
    auth_data = talloc_zero(req->be_ctx, struct authtok_conv);
    conv.appdata_ptr=auth_data;

    ret = pam_start("sssd_be_test", pd->user, &conv, &pamh);
    if (ret == PAM_SUCCESS) {
        DEBUG(1, ("Pam transaction started.\n"));
        pam_set_item(pamh, PAM_TTY, pd->tty);
        if (ret != PAM_SUCCESS) {
            DEBUG(1, ("Setting PAM_TTY failed: %s.\n", pam_strerror(pamh, ret)));
        }
        pam_set_item(pamh, PAM_RUSER, pd->ruser);
        if (ret != PAM_SUCCESS) {
            DEBUG(1, ("Setting PAM_RUSER failed: %s.\n", pam_strerror(pamh, ret)));
        }
        pam_set_item(pamh, PAM_RHOST, pd->rhost);
        if (ret != PAM_SUCCESS) {
            DEBUG(1, ("Setting PAM_RHOST failed: %s.\n", pam_strerror(pamh, ret)));
        }
        switch (pd->cmd) {
            case SSS_PAM_AUTHENTICATE:
/* FIXME: \0 missing at the end */
                auth_data->authtok=(char *) pd->authtok;
                auth_data->oldauthtok=NULL;
                pam_status=pam_authenticate(pamh, 0);
                break;
            case SSS_PAM_SETCRED:
                pam_status=pam_setcred(pamh, 0);
                break;
            case SSS_PAM_ACCT_MGMT:
                pam_status=pam_acct_mgmt(pamh, 0);
                break;
            case SSS_PAM_OPEN_SESSION:
                pam_status=pam_open_session(pamh, 0);
                break;
            case SSS_PAM_CLOSE_SESSION:
                pam_status=pam_close_session(pamh, 0);
                break;
            case SSS_PAM_CHAUTHTOK:
/* FIXME: \0 missing at the end */
                auth_data->authtok=(char *) pd->newauthtok;
                auth_data->oldauthtok=(char *) pd->authtok;
                pam_status=pam_chauthtok(pamh, 0);
                break;
            default:
                DEBUG(1, ("unknown PAM call"));
                pam_status=PAM_ABORT;
        }

        DEBUG(4, ("Pam result: [%d][%s]\n", pam_status, pam_strerror(pamh, pam_status)));

        ret = pam_end(pamh, pam_status);
        if (ret != PAM_SUCCESS) {
            pamh=NULL;
            DEBUG(1, ("Cannot terminate pam transaction.\n"));
        }

    } else {
        DEBUG(1, ("Failed to initialize pam transaction.\n"));
        pam_status = PAM_SYSTEM_ERR;
    }

    talloc_free(auth_data);

    pd->pam_status = pam_status;
    req->fn(req, EOK, NULL);
}

static void proxy_reply(struct be_req *req, int error, const char *errstr)
{
    return req->fn(req, error, errstr);
}

struct proxy_data {
    struct sysdb_req *sysreq;
    struct proxy_ctx *ctx;
    struct be_req *req;

    char *buffer;
    size_t buflen;

    struct passwd *pwd;
    struct group *grp;

    gid_t *groups;
    long int num;
    long int cur;

    struct ldb_dn *dn;

    sysdb_callback_t next_fn;
};

static void proxy_return(void *pvt, int error, struct ldb_result *ignore)
{
    struct proxy_data *data = talloc_get_type(pvt, struct proxy_data);
    const char *err = "Success";

    if (error != EOK) err = "Operation failed";

    sysdb_transaction_done(data->sysreq, error);
    return proxy_reply(data->req, error, err);
}

static void del_db_entry(struct sysdb_req *req, void *pvt)
{
    struct proxy_data *data = talloc_get_type(pvt, struct proxy_data);
    struct sysdb_ctx *ctx;
    int ret;

    data->sysreq = req;
    ctx = sysdb_req_get_ctx(req);

    ret = sysdb_delete_entry(req, data->dn, data->next_fn, data);
    if (ret != EOK) {
        proxy_return(data, ret, NULL);
    }
}

static void del_pw_uid(struct sysdb_req *req, void *pvt)
{
    struct proxy_data *data = talloc_get_type(pvt, struct proxy_data);
    struct sysdb_ctx *ctx;
    int ret;

    data->sysreq = req;
    ctx = sysdb_req_get_ctx(req);

    ret = sysdb_delete_user_by_uid(req,
                                   data->req->be_ctx->domain,
                                   data->pwd->pw_uid,
                                   data->next_fn, data);
    if (ret != EOK) {
        proxy_return(data, ret, NULL);
    }
}

static void set_pw_name(struct sysdb_req *req, void *pvt)
{
    struct proxy_data *data = talloc_get_type(pvt, struct proxy_data);
    struct sysdb_ctx *ctx;
    int ret;

    data->sysreq = req;
    ctx = sysdb_req_get_ctx(req);

    ret = sysdb_legacy_store_user(req, data->req->be_ctx->domain,
                                  data->pwd->pw_name, data->pwd->pw_passwd,
                                  data->pwd->pw_uid, data->pwd->pw_gid,
                                  data->pwd->pw_gecos, data->pwd->pw_dir,
                                  data->pwd->pw_shell,
                                  data->next_fn, data);
    if (ret != EOK) {
        proxy_return(data, ret, NULL);
    }
}

static void get_pw_name(struct be_req *req, char *name)
{
    struct proxy_ctx *ctx;
    enum nss_status status;
    struct proxy_data *data;
    int ret;

    ctx = talloc_get_type(req->be_ctx->pvt_data, struct proxy_ctx);

    data = talloc_zero(req, struct proxy_data);
    if (!data)
        return proxy_reply(req, ENOMEM, "Out of memory");
    data->req = req;
    data->ctx = ctx;
    data->next_fn = proxy_return;
    data->pwd = talloc(data, struct passwd);
    if (!data->pwd)
        return proxy_reply(req, ENOMEM, "Out of memory");

    data->buflen = 4096;
    data->buffer = talloc_size(data, data->buflen);
    if (!data->buffer)
        return proxy_reply(req, ENOMEM, "Out of memory");

    status = ctx->ops.getpwnam_r(name, data->pwd,
                                 data->buffer, data->buflen, &ret);

    switch (status) {
    case NSS_STATUS_NOTFOUND:
        data->dn = sysdb_user_dn(req->be_ctx->sysdb, data,
                                 req->be_ctx->domain, name);
        if (!data->dn)
            return proxy_reply(req, ENOMEM, "Out of memory");

        ret = sysdb_transaction(data, req->be_ctx->sysdb, del_db_entry, data);
        break;

    case NSS_STATUS_SUCCESS:
        ret = sysdb_transaction(data, req->be_ctx->sysdb, set_pw_name, data);
        break;

    default:
        DEBUG(2, ("proxy -> getpwnam_r failed for '%s' (%d)[%s]\n",
                  name, ret, strerror(ret)));
        return proxy_reply(req, ret, "Operation failed");
    }

    if (ret != EOK) {
        DEBUG(1, ("Failed to start transaction (%d)[%s]!?\n",
                  ret, strerror(ret)));
        return proxy_reply(req, ret, "Operation failed");
    }
}

static void get_pw_uid(struct be_req *req, uid_t uid)
{
    struct proxy_ctx *ctx;
    enum nss_status status;
    struct proxy_data *data;
    int ret;

    ctx = talloc_get_type(req->be_ctx->pvt_data, struct proxy_ctx);

    data = talloc_zero(req, struct proxy_data);
    if (!data)
        return proxy_reply(req, ENOMEM, "Out of memory");
    data->req = req;
    data->ctx = ctx;
    data->next_fn = proxy_return;
    data->pwd = talloc(data, struct passwd);
    if (!data->pwd)
        return proxy_reply(req, ENOMEM, "Out of memory");

    data->buflen = 4096;
    data->buffer = talloc_size(data, data->buflen);
    if (!data->buffer)
        return proxy_reply(req, ENOMEM, "Out of memory");

    status = ctx->ops.getpwuid_r(uid, data->pwd,
                                 data->buffer, data->buflen, &ret);

    switch (status) {
    case NSS_STATUS_NOTFOUND:
        data->pwd->pw_uid = uid;
        ret = sysdb_transaction(data, req->be_ctx->sysdb, del_pw_uid, data);
        break;

    case NSS_STATUS_SUCCESS:
        ret = sysdb_transaction(data, req->be_ctx->sysdb, set_pw_name, data);
        break;

    default:
        DEBUG(2, ("proxy -> getpwuid_r failed for '%lu' (%d)[%s]\n",
                  (unsigned long)uid, ret, strerror(ret)));
        return proxy_reply(req, ret, "Operation failed");
    }

    if (ret != EOK) {
        DEBUG(1, ("Failed to start transaction (%d)[%s]!?\n",
                  ret, strerror(ret)));
        return proxy_reply(req, ret, "Operation failed");
    }
}

#define MAX_BUF_SIZE 1024*1024 /* max 1MiB */

static void get_pw_entry(struct sysdb_req *req, void *pvt);

static void get_next_pw_entry(void *pvt, int error, struct ldb_result *ignore)
{
    struct proxy_data *data = talloc_get_type(pvt, struct proxy_data);

    if (error != EOK) proxy_return(data, error, NULL);

    get_pw_entry(data->sysreq, data);
}

static void get_pw_entry(struct sysdb_req *req, void *pvt)
{
    struct proxy_data *data = talloc_get_type(pvt, struct proxy_data);
    enum nss_status status;
    struct sysdb_ctx *ctx;
    char *newb;
    int ret;

    data->sysreq = req;
    ctx = sysdb_req_get_ctx(req);

retry:
    status = data->ctx->ops.getpwent_r(data->pwd,
                                       data->buffer, data->buflen, &ret);

    switch (status) {
    case NSS_STATUS_TRYAGAIN:
        /* buffer too small ? */
        if (data->buflen < MAX_BUF_SIZE) {
            data->buflen *= 2;
        }
        if (data->buflen > MAX_BUF_SIZE) {
            data->buflen = MAX_BUF_SIZE;
        }
        newb = talloc_realloc_size(data, data->buffer, data->buflen);
        if (!newb) {
            return proxy_return(data, ENOMEM, NULL);
        }
        data->buffer = newb;
        goto retry;

    case NSS_STATUS_NOTFOUND:

        data->ctx->ops.endpwent();
        data->next_fn(data, EOK, NULL);
        break;

    case NSS_STATUS_SUCCESS:
        ret = sysdb_legacy_store_user(req, data->req->be_ctx->domain,
                                      data->pwd->pw_name,
                                      data->pwd->pw_passwd,
                                      data->pwd->pw_uid,
                                      data->pwd->pw_gid,
                                      data->pwd->pw_gecos,
                                      data->pwd->pw_dir,
                                      data->pwd->pw_shell,
                                      get_next_pw_entry, data);
        if (ret != EOK) {
            DEBUG(1, ("Failed to update LDB Cache for '%s' (%d)[%s] !?\n",
                      data->pwd->pw_name, ret, strerror(ret)));
            proxy_return(data, ret, NULL);
        }
        break;

    default:
        DEBUG(2, ("proxy -> getpwent_r failed (%d)[%s]\n",
                  ret, strerror(ret)));
        proxy_return(data, ret, NULL);
    }
}

static void enum_users(struct be_req *req)
{
    struct proxy_ctx *ctx;
    enum nss_status status;
    struct proxy_data *data;
    int ret;

    ctx = talloc_get_type(req->be_ctx->pvt_data, struct proxy_ctx);

    data = talloc_zero(req, struct proxy_data);
    if (!data)
        return proxy_reply(req, ENOMEM, "Out of memory");
    data->req = req;
    data->ctx = ctx;
    data->next_fn = proxy_return;
    data->pwd = talloc(data, struct passwd);
    if (!data->pwd)
        return proxy_reply(req, ENOMEM, "Out of memory");

    data->buflen = 4096;
    data->buffer = talloc_size(data, data->buflen);
    if (!data->buffer)
        return proxy_reply(req, ENOMEM, "Out of memory");

    status = ctx->ops.setpwent();
    if (status != NSS_STATUS_SUCCESS)
        return proxy_reply(req, EIO, "Operation failed");

    ret = sysdb_transaction(data, req->be_ctx->sysdb, get_pw_entry, data);
    if (ret != EOK) {
        DEBUG(1, ("Failed to start transaction (%d)[%s]!?\n",
                  ret, strerror(ret)));
        return proxy_reply(req, ret, "Operation failed");
    }
}

static void del_gr_uid(struct sysdb_req *req, void *pvt)
{
    struct proxy_data *data = talloc_get_type(pvt, struct proxy_data);
    struct sysdb_ctx *ctx;
    int ret;

    data->sysreq = req;
    ctx = sysdb_req_get_ctx(req);

    ret = sysdb_delete_group_by_gid(req,
                                    data->req->be_ctx->domain,
                                    data->grp->gr_gid,
                                    data->next_fn, data);
    if (ret != EOK) {
        proxy_return(data, ret, NULL);
    }
}

static void set_gr_name(struct sysdb_req *req, void *pvt)
{
    struct proxy_data *data = talloc_get_type(pvt, struct proxy_data);
    struct sysdb_ctx *ctx;
    int ret;

    data->sysreq = req;
    ctx = sysdb_req_get_ctx(req);

    ret = sysdb_legacy_store_group(req, data->req->be_ctx->domain,
                                   data->grp->gr_name,
                                   data->grp->gr_gid,
                                   (const char **)data->grp->gr_mem,
                                   data->next_fn, data);
    if (ret != EOK) {
        proxy_return(data, ret, NULL);
    }
}

static void get_gr_name(struct be_req *req, char *name)
{
    struct proxy_ctx *ctx;
    enum nss_status status;
    struct proxy_data *data;
    int ret;

    ctx = talloc_get_type(req->be_ctx->pvt_data, struct proxy_ctx);

    data = talloc_zero(req, struct proxy_data);
    if (!data)
        return proxy_reply(req, ENOMEM, "Out of memory");
    data->req = req;
    data->ctx = ctx;
    data->next_fn = proxy_return;
    data->grp = talloc(data, struct group);
    if (!data->grp)
        return proxy_reply(req, ENOMEM, "Out of memory");

    data->buflen = 4096;
    data->buffer = talloc_size(data, data->buflen);
    if (!data->buffer)
        return proxy_reply(req, ENOMEM, "Out of memory");

    status = ctx->ops.getgrnam_r(name, data->grp,
                                 data->buffer, data->buflen, &ret);

    switch (status) {
    case NSS_STATUS_NOTFOUND:
        data->dn = sysdb_group_dn(req->be_ctx->sysdb, data,
                                  req->be_ctx->domain, name);
        if (!data->dn)
            return proxy_reply(req, ENOMEM, "Out of memory");

        ret = sysdb_transaction(data, req->be_ctx->sysdb, del_db_entry, data);
        break;

    case NSS_STATUS_SUCCESS:
        ret = sysdb_transaction(data, req->be_ctx->sysdb, set_gr_name, data);
        break;

    default:
        DEBUG(2, ("proxy -> getgrnam_r failed for '%s' (%d)[%s]\n",
                  name, ret, strerror(ret)));
        return proxy_reply(req, ret, "Operation failed");
    }

    if (ret != EOK) {
        DEBUG(1, ("Failed to start transaction (%d)[%s]!?\n",
                  ret, strerror(ret)));
        return proxy_reply(req, ret, "Operation failed");
    }
}

static void get_gr_gid(struct be_req *req, gid_t gid)
{
    struct proxy_ctx *ctx;
    enum nss_status status;
    struct proxy_data *data;
    int ret;

    ctx = talloc_get_type(req->be_ctx->pvt_data, struct proxy_ctx);

    data = talloc_zero(req, struct proxy_data);
    if (!data)
        return proxy_reply(req, ENOMEM, "Out of memory");
    data->req = req;
    data->ctx = ctx;
    data->next_fn = proxy_return;
    data->grp = talloc(data, struct group);
    if (!data->grp)
        return proxy_reply(req, ENOMEM, "Out of memory");

    data->buflen = 4096;
    data->buffer = talloc_size(data, data->buflen);
    if (!data->buffer)
        return proxy_reply(req, ENOMEM, "Out of memory");

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

    switch (status) {
    case NSS_STATUS_NOTFOUND:
        data->grp->gr_gid = gid;
        ret = sysdb_transaction(data, req->be_ctx->sysdb, del_gr_uid, data);
        break;

    case NSS_STATUS_SUCCESS:
        ret = sysdb_transaction(data, req->be_ctx->sysdb, set_gr_name, data);
        break;

    default:
        DEBUG(2, ("proxy -> getgrgid_r failed for '%lu' (%d)[%s]\n",
                  (unsigned long)gid, ret, strerror(ret)));
        return proxy_reply(req, ret, "Operation failed");
    }

    if (ret != EOK) {
        DEBUG(1, ("Failed to start transaction (%d)[%s]!?\n",
                  ret, strerror(ret)));
        return proxy_reply(req, ret, "Operation failed");
    }
}

static void get_gr_entry(struct sysdb_req *req, void *pvt);

static void get_next_gr_entry(void *pvt, int error, struct ldb_result *ignore)
{
    struct proxy_data *data = talloc_get_type(pvt, struct proxy_data);

    if (error != EOK) proxy_return(data, error, NULL);

    get_gr_entry(data->sysreq, data);
}

static void get_gr_entry(struct sysdb_req *req, void *pvt)
{
    struct proxy_data *data = talloc_get_type(pvt, struct proxy_data);
    enum nss_status status;
    struct sysdb_ctx *ctx;
    char *newb;
    int ret;

    data->sysreq = req;
    ctx = sysdb_req_get_ctx(req);

retry:
    status = data->ctx->ops.getgrent_r(data->grp,
                                       data->buffer, data->buflen, &ret);

    switch (status) {
    case NSS_STATUS_TRYAGAIN:
        /* buffer too small ? */
        if (data->buflen < MAX_BUF_SIZE) {
            data->buflen *= 2;
        }
        if (data->buflen > MAX_BUF_SIZE) {
            data->buflen = MAX_BUF_SIZE;
        }
        newb = talloc_realloc_size(data, data->buffer, data->buflen);
        if (!newb) {
            return proxy_return(data, ENOMEM, NULL);
        }
        data->buffer = newb;
        goto retry;

    case NSS_STATUS_NOTFOUND:

        data->ctx->ops.endgrent();
        data->next_fn(data, EOK, NULL);
        break;

    case NSS_STATUS_SUCCESS:
        ret = sysdb_legacy_store_group(req, data->req->be_ctx->domain,
                                       data->grp->gr_name,
                                       data->grp->gr_gid,
                                       (const char **)data->grp->gr_mem,
                                       get_next_gr_entry, data);
        if (ret != EOK) {
            DEBUG(1, ("Failed to update LDB Cache for '%s' (%d)[%s] !?\n",
                      data->grp->gr_name, ret, strerror(ret)));
            proxy_return(data, ret, NULL);
        }
        break;

    default:
        DEBUG(2, ("proxy -> getgrent_r failed (%d)[%s]\n",
                  ret, strerror(ret)));
        proxy_return(data, ret, NULL);
    }
}

static void enum_groups(struct be_req *req)
{
    struct proxy_ctx *ctx;
    enum nss_status status;
    struct proxy_data *data;
    int ret;

    ctx = talloc_get_type(req->be_ctx->pvt_data, struct proxy_ctx);

    data = talloc_zero(req, struct proxy_data);
    if (!data)
        return proxy_reply(req, ENOMEM, "Out of memory");
    data->req = req;
    data->ctx = ctx;
    data->next_fn = proxy_return;
    data->grp = talloc(data, struct group);
    if (!data->grp)
        return proxy_reply(req, ENOMEM, "Out of memory");

    data->buflen = 4096;
    data->buffer = talloc_size(data, data->buflen);
    if (!data->buffer)
        return proxy_reply(req, ENOMEM, "Out of memory");

    status = ctx->ops.setgrent();
    if (status != NSS_STATUS_SUCCESS)
        return proxy_reply(req, EIO, "Operation failed");

    ret = sysdb_transaction(data, req->be_ctx->sysdb, get_gr_entry, data);
    if (ret != EOK) {
        DEBUG(1, ("Failed to start transaction (%d)[%s]!?\n",
                  ret, strerror(ret)));
        return proxy_reply(req, ret, "Operation failed");
    }
}

static void get_gid_entry(struct sysdb_req *req, void *pvt);

static void get_next_gid_entry(void *pvt, int error, struct ldb_result *ignore)
{
    struct proxy_data *data = talloc_get_type(pvt, struct proxy_data);

    if (error != EOK) proxy_return(data, error, NULL);

    get_gid_entry(data->sysreq, data);
}

static void get_gid_entry(struct sysdb_req *req, void *pvt)
{
    struct proxy_data *data = talloc_get_type(pvt, struct proxy_data);
    enum nss_status status;
    struct sysdb_ctx *ctx;
    char *newb;
    int ret;

    ctx = sysdb_req_get_ctx(req);

    /* all done */
    if (data->cur == data->num)
        return data->next_fn(data, EOK, NULL);

retry:
    status = data->ctx->ops.getgrgid_r(data->groups[data->cur], data->grp,
                                       data->buffer, data->buflen, &ret);

    switch (status) {
    case NSS_STATUS_TRYAGAIN:
        /* buffer too small ? */
        if (data->buflen < MAX_BUF_SIZE) {
            data->buflen *= 2;
        }
        if (data->buflen > MAX_BUF_SIZE) {
            data->buflen = MAX_BUF_SIZE;
        }
        newb = talloc_realloc_size(data, data->buffer, data->buflen);
        if (!newb) {
            return proxy_return(data, ENOMEM, NULL);
        }
        data->buffer = newb;
        goto retry;

    case NSS_STATUS_NOTFOUND:
        data->cur++;
        DEBUG(4, ("gid [%lu] not found, removing group\n",
                  (unsigned long)(data->groups[data->cur])));
        ret = sysdb_delete_group_by_gid(req, data->req->be_ctx->domain,
                                        data->groups[data->cur-1],
                                        get_next_gid_entry, data);
        if (ret != EOK) {
            DEBUG(1, ("Failed to update LDB Cache for '%s' (%d)[%s] !?\n",
                      data->grp->gr_name, ret, strerror(ret)));
            proxy_return(data, ret, NULL);
        }
        break;

    case NSS_STATUS_SUCCESS:
        data->cur++;
        ret = sysdb_legacy_store_group(req, data->req->be_ctx->domain,
                                       data->grp->gr_name,
                                       data->grp->gr_gid,
                                       (const char **)data->grp->gr_mem,
                                       get_next_gid_entry, data);
        if (ret != EOK) {
            DEBUG(1, ("Failed to update LDB Cache for '%s' (%d)[%s] !?\n",
                      data->grp->gr_name, ret, strerror(ret)));
            proxy_return(data, ret, NULL);
        }
        break;

    default:
        DEBUG(2, ("proxy -> getgrgid_r failed (%d)[%s]\n",
                  ret, strerror(ret)));
        proxy_return(data, ret, NULL);
    }
}

static void get_user_groups(void *pvt, int error, struct ldb_result *ignore)
{
    struct proxy_data *data = talloc_get_type(pvt, struct proxy_data);
    enum nss_status status;
    long int limit;
    long int start;
    long int size;
    long int num;
    char *name;
    gid_t gid;
    int ret;

    if (error != EOK) proxy_return(data, error, NULL);
    data->next_fn = proxy_return;

    start = 0;
    limit = 4096;
    num = 4096;
    size = num*sizeof(gid_t);
    data->groups = talloc_size(data, size);
    if (!data->groups)
        return proxy_return(data, ENOMEM, NULL);

    gid = data->pwd->pw_gid;
    name = talloc_strdup(data, data->pwd->pw_name);
    if (!name)
        return proxy_return(data, ENOMEM, NULL);

retry:
    status = data->ctx->ops.initgroups_dyn(name, gid,
                                           &start, &num,
                                           &data->groups, limit, &ret);
    switch (status) {
    case 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;
        data->groups = talloc_realloc_size(data, data->groups, size);
        if (!data->groups) {
            return proxy_return(data, ENOMEM, NULL);
        }
        goto retry;

    case NSS_STATUS_SUCCESS:
        data->num = start;
        DEBUG(4, ("User [%s] appears to be member of %lu groups\n",
                  name, data->num));
        get_gid_entry(data->sysreq, data);
        break;

    default:
        DEBUG(2, ("proxy -> getgrent_r failed (%d)[%s]\n",
                  ret, strerror(ret)));
        proxy_return(data, ret, NULL);
    }
}

static void get_initgr_user(struct be_req *req, char *name)
{
    struct proxy_ctx *ctx;
    enum nss_status status;
    struct proxy_data *data;
    int ret;

    ctx = talloc_get_type(req->be_ctx->pvt_data, struct proxy_ctx);

    data = talloc_zero(req, struct proxy_data);
    if (!data)
        return proxy_reply(req, ENOMEM, "Out of memory");
    data->req = req;
    data->ctx = ctx;
    data->next_fn = proxy_return;
    data->pwd = talloc(data, struct passwd);
    if (!data->pwd)
        return proxy_reply(req, ENOMEM, "Out of memory");
    data->grp = talloc(data, struct group);
    if (!data->grp)
        return proxy_reply(req, ENOMEM, "Out of memory");

    data->buflen = 4096;
    data->buffer = talloc_size(data, data->buflen);
    if (!data->buffer)
        return proxy_reply(req, ENOMEM, "Out of memory");

    status = ctx->ops.getpwnam_r(name, data->pwd,
                                 data->buffer, data->buflen, &ret);

    switch (status) {
    case NSS_STATUS_NOTFOUND:
        data->dn = sysdb_user_dn(req->be_ctx->sysdb, data,
                                 req->be_ctx->domain, name);
        if (!data->dn)
            return proxy_reply(req, ENOMEM, "Out of memory");

        ret = sysdb_transaction(data, req->be_ctx->sysdb, del_db_entry, data);
        break;

    case NSS_STATUS_SUCCESS:
        data->next_fn = get_user_groups;
        ret = sysdb_transaction(data, req->be_ctx->sysdb, set_pw_name, data);
        break;

    default:
        DEBUG(2, ("proxy -> getpwnam_r failed for '%s' (%d)[%s]\n",
                  name, ret, strerror(ret)));
        return proxy_reply(req, ret, "Operation failed");
    }

    if (ret != EOK) {
        DEBUG(1, ("Failed to start transaction (%d)[%s]!?\n",
                  ret, strerror(ret)));
        return proxy_reply(req, ret, "Operation failed");
    }
}

/* TODO: actually do check something */
static void proxy_check_online(struct be_req *req)
{
    struct be_online_req *oreq;

    oreq = talloc_get_type(req->req_data, struct be_online_req);

    oreq->online = MOD_ONLINE;

    req->fn(req, EOK, NULL);
}

/* TODO: See if we can use async_req code */
static void proxy_get_account_info(struct be_req *req)
{
    struct be_acct_req *ar;
    uid_t uid;
    gid_t gid;

    ar = talloc_get_type(req->req_data, struct be_acct_req);

    switch (ar->entry_type) {
    case BE_REQ_USER: /* user */
        switch (ar->filter_type) {
        case BE_FILTER_NAME:
            switch (ar->attr_type) {
            case BE_ATTR_CORE:
                if (strchr(ar->filter_value, '*')) {
                    return enum_users(req);
                } else {
                    return get_pw_name(req, ar->filter_value);
                }
                break;
            default:
                return proxy_reply(req, EINVAL, "Invalid attr type");
            }
            break;
        case BE_FILTER_IDNUM:
            switch (ar->attr_type) {
            case BE_ATTR_CORE:
                if (strchr(ar->filter_value, '*')) {
                    return proxy_reply(req, EINVAL, "Invalid attr type");
                } else {
                    char *endptr;
                    errno = 0;
                    uid = (uid_t)strtol(ar->filter_value, &endptr, 0);
                    if (errno || *endptr || (ar->filter_value == endptr)) {
                        return proxy_reply(req, EINVAL, "Invalid attr type");
                    }
                    return get_pw_uid(req, uid);
                }
                break;
            default:
                return proxy_reply(req, EINVAL, "Invalid attr type");
            }
            break;
        default:
            return proxy_reply(req, EINVAL, "Invalid filter type");
        }
        break;

    case BE_REQ_GROUP: /* group */
        switch (ar->filter_type) {
        case BE_FILTER_NAME:
            switch (ar->attr_type) {
            case BE_ATTR_CORE:
                if (strchr(ar->filter_value, '*')) {
                    return enum_groups(req);
                } else {
                    return get_gr_name(req, ar->filter_value);
                }
                break;
            default:
                return proxy_reply(req, EINVAL, "Invalid attr type");
            }
            break;
        case BE_FILTER_IDNUM:
            switch (ar->attr_type) {
            case BE_ATTR_CORE:
                if (strchr(ar->filter_value, '*')) {
                    return proxy_reply(req, EINVAL, "Invalid attr type");
                } else {
                    char *endptr;
                    errno = 0;
                    gid = (gid_t)strtol(ar->filter_value, &endptr, 0);
                    if (errno || *endptr || (ar->filter_value == endptr)) {
                        return proxy_reply(req, EINVAL, "Invalid attr type");
                    }
                    return get_gr_gid(req, gid);
                }
                break;
            default:
                return proxy_reply(req, EINVAL, "Invalid attr type");
            }
            break;
        default:
            return proxy_reply(req, EINVAL, "Invalid filter type");
        }
        break;

    case BE_REQ_INITGROUPS: /* init groups for user */
        if (ar->filter_type != BE_FILTER_NAME) {
            return proxy_reply(req, EINVAL, "Invalid filter type");
        }
        if (ar->attr_type != BE_ATTR_CORE) {
            return proxy_reply(req, EINVAL, "Invalid attr type");
        }
        if (strchr(ar->filter_value, '*')) {
            return proxy_reply(req, EINVAL, "Invalid filter value");
        }
        return get_initgr_user(req, ar->filter_value);

    default: /*fail*/
        return proxy_reply(req, EINVAL, "Invalid request type");
    }
}

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

struct be_mod_ops proxy_mod_ops = {
    .check_online = proxy_check_online,
    .get_account_info = proxy_get_account_info,
    .pam_handler = proxy_pam_handler,
    .finalize = proxy_shutdown
};

static void *proxy_dlsym(void *handle, const char *functemp, char *libname)
{
    char *funcname;
    void *funcptr;

    funcname = talloc_asprintf(NULL, functemp, libname);
    if (funcname == NULL) return NULL;

    funcptr = dlsym(handle, funcname);
    talloc_free(funcname);

    return funcptr;
}

int sssm_proxy_init(struct be_ctx *bectx, struct be_mod_ops **ops, void **pvt_data)
{
    struct proxy_ctx *ctx;
    char *libname;
    char *libpath;
    void *handle;
    int ret;

    ctx = talloc(bectx, struct proxy_ctx);
    if (!ctx) {
        return ENOMEM;
    }

    ret = confdb_get_string(bectx->cdb, ctx, bectx->conf_path,
                           "libName", NULL, &libname);
    ret = confdb_get_string(bectx->cdb, ctx, bectx->conf_path,
                           "libPath", NULL, &libpath);
    if (ret != EOK) goto done;
    if (libpath == NULL || libname == NULL) {
        ret = ENOENT;
        goto done;
    }

    handle = dlopen(libpath, RTLD_NOW);
    if (!handle) {
        DEBUG(0, ("Unable to load %s module with path, error: %s\n",
                  libpath, dlerror()));
        ret = ELIBACC;
        goto done;
    }

    ctx->ops.getpwnam_r = proxy_dlsym(handle, "_nss_%s_getpwnam_r", libname);
    if (!ctx->ops.getpwnam_r) {
        DEBUG(0, ("Failed to load NSS fns, error: %s\n", dlerror()));
        ret = ELIBBAD;
        goto done;
    }

    ctx->ops.getpwuid_r = proxy_dlsym(handle, "_nss_%s_getpwuid_r", libname);
    if (!ctx->ops.getpwuid_r) {
        DEBUG(0, ("Failed to load NSS fns, error: %s\n", dlerror()));
        ret = ELIBBAD;
        goto done;
    }

    ctx->ops.setpwent = proxy_dlsym(handle, "_nss_%s_setpwent", libname);
    if (!ctx->ops.setpwent) {
        DEBUG(0, ("Failed to load NSS fns, error: %s\n", dlerror()));
        ret = ELIBBAD;
        goto done;
    }

    ctx->ops.getpwent_r = proxy_dlsym(handle, "_nss_%s_getpwent_r", libname);
    if (!ctx->ops.getpwent_r) {
        DEBUG(0, ("Failed to load NSS fns, error: %s\n", dlerror()));
        ret = ELIBBAD;
        goto done;
    }

    ctx->ops.endpwent = proxy_dlsym(handle, "_nss_%s_endpwent", libname);
    if (!ctx->ops.endpwent) {
        DEBUG(0, ("Failed to load NSS fns, error: %s\n", dlerror()));
        ret = ELIBBAD;
        goto done;
    }

    ctx->ops.getgrnam_r = proxy_dlsym(handle, "_nss_%s_getgrnam_r", libname);
    if (!ctx->ops.getgrnam_r) {
        DEBUG(0, ("Failed to load NSS fns, error: %s\n", dlerror()));
        ret = ELIBBAD;
        goto done;
    }

    ctx->ops.getgrgid_r = proxy_dlsym(handle, "_nss_%s_getgrgid_r", libname);
    if (!ctx->ops.getgrgid_r) {
        DEBUG(0, ("Failed to load NSS fns, error: %s\n", dlerror()));
        ret = ELIBBAD;
        goto done;
    }

    ctx->ops.setgrent = proxy_dlsym(handle, "_nss_%s_setgrent", libname);
    if (!ctx->ops.setgrent) {
        DEBUG(0, ("Failed to load NSS fns, error: %s\n", dlerror()));
        ret = ELIBBAD;
        goto done;
    }

    ctx->ops.getgrent_r = proxy_dlsym(handle, "_nss_%s_getgrent_r", libname);
    if (!ctx->ops.getgrent_r) {
        DEBUG(0, ("Failed to load NSS fns, error: %s\n", dlerror()));
        ret = ELIBBAD;
        goto done;
    }

    ctx->ops.endgrent = proxy_dlsym(handle, "_nss_%s_endgrent", libname);
    if (!ctx->ops.endgrent) {
        DEBUG(0, ("Failed to load NSS fns, error: %s\n", dlerror()));
        ret = ELIBBAD;
        goto done;
    }

    ctx->ops.initgroups_dyn = proxy_dlsym(handle, "_nss_%s_initgroups_dyn",
                                                  libname);
    if (!ctx->ops.initgroups_dyn) {
        DEBUG(0, ("Failed to load NSS fns, error: %s\n", dlerror()));
        ret = ELIBBAD;
        goto done;
    }

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

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