/*
 * Copyright 2013-2017 Red Hat, Inc.
 *
 * 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; version 2 of the License.
 *
 * 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, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307 USA
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <dlfcn.h>
#include <errno.h>
#include <pwd.h>
#include <grp.h>
#include <sys/param.h>
#include "back_extdom.h"

struct nss_ops_ctx {
    void *dl_handle;
    long int initgroups_start;

    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 (*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 (*initgroups_dyn)(const char *user, gid_t group,
                                      long int *start, long int *size,
                                      gid_t **groups, long int limit,
                                      int *errnop);
};

void back_extdom_free_context(struct nss_ops_ctx **nss_context)
{
    if ((nss_context == NULL) || (*nss_context == NULL)) {
        return;
    }

    if ((*nss_context)->dl_handle != NULL) {
        dlclose((*nss_context)->dl_handle);
    }

    free((*nss_context));
    *nss_context = NULL;
}

int back_extdom_init_context(struct nss_ops_ctx **nss_context)
{
    struct nss_ops_ctx *ctx = NULL;

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

    ctx = calloc(1, sizeof(struct nss_ops_ctx));
    if (ctx == NULL) {
        return ENOMEM;
    }
    *nss_context = ctx;

    ctx->dl_handle = dlopen("libnss_sss.so.2", RTLD_NOW);
    if (ctx->dl_handle == NULL) {
        goto fail;
    }

    ctx->getpwnam_r = dlsym(ctx->dl_handle, "_nss_sss_getpwnam_r");
    if (ctx->getpwnam_r == NULL) {
        goto fail;
    }

    ctx->getpwuid_r = dlsym(ctx->dl_handle, "_nss_sss_getpwuid_r");
    if (ctx->getpwuid_r == NULL) {
        goto fail;
    }

    ctx->getgrnam_r = dlsym(ctx->dl_handle, "_nss_sss_getgrnam_r");
    if (ctx->getgrnam_r == NULL) {
        goto fail;
    }

    ctx->getgrgid_r = dlsym(ctx->dl_handle, "_nss_sss_getgrgid_r");
    if (ctx->getgrgid_r == NULL) {
        goto fail;
    }

    ctx->initgroups_dyn = dlsym(ctx->dl_handle, "_nss_sss_initgroups_dyn");
    if (ctx->initgroups_dyn == NULL) {
        goto fail;
    }

    return 0;

fail:
    back_extdom_free_context(nss_context);

    return EINVAL;
}


/* Following three functions cannot be implemented with nss_sss.so.2
 * As result, we simply do nothing here */

void back_extdom_set_timeout(struct nss_ops_ctx *nss_context,
                             unsigned int timeout) {
        /* no operation */
}

void back_extdom_evict_user(struct nss_ops_ctx *nss_context,
                            const char *name) {
        /* no operation */
}

void back_extdom_evict_group(struct nss_ops_ctx *nss_context,
                             const char *name) {
        /* no operation */
}

enum nss_status back_extdom_getpwnam(struct nss_ops_ctx *nss_context,
                                     const char *name, struct passwd *pwd,
                                     char *buffer, size_t buflen,
                                     struct passwd **result,
                                     int *lerrno) {
    enum nss_status ret;

    if (nss_context == NULL) {
        return NSS_STATUS_UNAVAIL;
    }

    ret = nss_context->getpwnam_r(name, pwd,
                                  buffer, buflen,
                                  lerrno);

    if ((ret == NSS_STATUS_SUCCESS) && (result != NULL)) {
        *result = pwd;
        *lerrno = 0;
    }

    return ret;
}

enum nss_status back_extdom_getpwuid(struct nss_ops_ctx *nss_context,
                                     uid_t uid, struct passwd *pwd,
                                     char *buffer, size_t buflen,
                                     struct passwd **result,
                                     int *lerrno) {
    enum nss_status ret;

    if (nss_context == NULL) {
        return NSS_STATUS_UNAVAIL;
    }

    ret = nss_context->getpwuid_r(uid, pwd,
                                  buffer, buflen,
                                  lerrno);

    if ((ret == NSS_STATUS_SUCCESS) && (result != NULL)) {
        *result = pwd;
        *lerrno = 0;
    }

    return ret;
}

enum nss_status back_extdom_getgrnam(struct nss_ops_ctx *nss_context,
                                     const char *name, struct group *grp,
                                     char *buffer, size_t buflen,
                                     struct group **result,
                                     int *lerrno) {
    enum nss_status ret;

    if (nss_context == NULL) {
        return NSS_STATUS_UNAVAIL;
    }

    ret = nss_context->getgrnam_r(name, grp,
                                  buffer, buflen,
                                  lerrno);

    if ((ret == NSS_STATUS_SUCCESS) && (result != NULL)) {
        *result = grp;
        *lerrno = 0;
    }

    return ret;
}

enum nss_status back_extdom_getgrgid(struct nss_ops_ctx *nss_context,
                                     gid_t gid, struct group *grp,
                                     char *buffer, size_t buflen,
                                     struct group **result,
                                     int *lerrno) {

    enum nss_status ret;

    if (nss_context == NULL) {
        return NSS_STATUS_UNAVAIL;
    }

    ret = nss_context->getgrgid_r(gid, grp,
                                  buffer, buflen,
                                  lerrno);

    if ((ret == NSS_STATUS_SUCCESS) && (result != NULL)) {
        *result = grp;
        *lerrno = 0;
    }

    return ret;
}

enum nss_status back_extdom_getgrouplist(struct nss_ops_ctx *nss_context,
                                         const char *name, gid_t group,
                                         gid_t *groups, int *ngroups,
                                         int *lerrno) {

    enum nss_status ret = NSS_STATUS_UNAVAIL;
    long int tsize = MAX (1, *ngroups);
    gid_t *newgroups = NULL;

    if (nss_context == NULL) {
        return NSS_STATUS_UNAVAIL;
    }

    newgroups = (gid_t *) calloc (tsize, sizeof (gid_t));
    if (newgroups == NULL) {
        *lerrno = ENOMEM;
        return NSS_STATUS_TRYAGAIN;
    }

    newgroups[0] = group;
    nss_context->initgroups_start = 1;

    ret = nss_context->initgroups_dyn(name, group,
                                      &nss_context->initgroups_start,
                                      &tsize, &newgroups,
                                      -1, lerrno);

    (void) memcpy(groups, newgroups,
                  MIN(*ngroups, nss_context->initgroups_start) * sizeof(gid_t));
    free(newgroups);

    if (*ngroups < nss_context->initgroups_start) {
        ret = NSS_STATUS_TRYAGAIN;
        *lerrno = ERANGE;
    }

    *ngroups = (int) nss_context->initgroups_start;

    nss_context->initgroups_start = 0;

    return ret;
}

