/*
   SSSD

   sss_groupshow

   Copyright (C) Jakub Hrozek <jhrozek@redhat.com>        2010

   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 <stdio.h>
#include <stdlib.h>
#include <talloc.h>
#include <popt.h>

#include "db/sysdb.h"
#include "util/util.h"
#include "tools/tools_util.h"
#include "tools/sss_sync_ops.h"

#define PADDING_SPACES   4
#define GROUP_SHOW_ATTRS { SYSDB_MEMBEROF, SYSDB_GIDNUM, \
                           SYSDB_MEMBER, SYSDB_GHOST, SYSDB_NAME, \
                           NULL }
#define GROUP_SHOW_MPG_ATTRS { SYSDB_MEMBEROF, SYSDB_UIDNUM, \
                                SYSDB_NAME, NULL }

struct group_info {
    const char *name;
    gid_t gid;
    bool  mpg;

    const char **user_members;
    const char **memberofs;

    struct group_info **group_members;
};

/*==================Helper routines to process results================= */
const char *rdn_as_string(TALLOC_CTX *mem_ctx,
                          struct ldb_dn *dn)
{
    const struct ldb_val *val;

    val = ldb_dn_get_rdn_val(dn);
    if (val == NULL) {
        return NULL;
    }

    return ldb_dn_escape_value(mem_ctx, *val);
}

static int parse_memberofs(struct ldb_context *ldb,
                           struct ldb_message_element *el,
                           struct group_info *gi)
{
    int i;
    struct ldb_dn *dn = NULL;

    gi->memberofs = talloc_array(gi, const char *, el->num_values+1);
    if (gi->memberofs == NULL) {
        return ENOMEM;
    }

    for (i = 0; i< el->num_values; ++i) {
        dn = ldb_dn_from_ldb_val(gi, ldb, &(el->values[i]));
        gi->memberofs[i] = talloc_strdup(gi, rdn_as_string(gi, dn));
        talloc_zfree(dn);
        if (gi->memberofs[i] == NULL) {
            return ENOMEM;
        }
        DEBUG(SSSDBG_TRACE_FUNC, "memberof value: %s\n", gi->memberofs[i]);
    }
    gi->memberofs[el->num_values] = NULL;

    return EOK;
}

static int parse_members(TALLOC_CTX *mem_ctx,
                         struct ldb_context *ldb,
                         struct sss_domain_info *domain,
                         struct ldb_message_element *el,
                         const  char *parent_name,
                         const  char ***user_members,
                         const  char ***group_members,
                         int    *num_group_members)
{
    struct ldb_dn *user_basedn = NULL, *group_basedn = NULL;
    struct ldb_dn *parent_dn = NULL;
    struct ldb_dn *dn = NULL;
    const char **um = NULL, **gm = NULL;
    unsigned int um_index = 0, gm_index = 0;
    TALLOC_CTX *tmp_ctx = NULL;
    int ret;
    int i;

    tmp_ctx = talloc_new(mem_ctx);
    if (!tmp_ctx) {
        ret = ENOMEM;
        goto fail;
    }

    user_basedn = sysdb_user_base_dn(tmp_ctx, domain);
    group_basedn = sysdb_group_base_dn(tmp_ctx, domain);
    if (!user_basedn || !group_basedn) {
        ret = ENOMEM;
        goto fail;
    }

    um = talloc_array(mem_ctx, const char *, el->num_values+1);
    gm = talloc_array(mem_ctx, const char *, el->num_values+1);
    if (!um || !gm) {
        ret = ENOMEM;
        goto fail;
    }

    for (i = 0; i< el->num_values; ++i) {
        dn = ldb_dn_from_ldb_val(tmp_ctx, ldb, &(el->values[i]));

        /* user member or group member? */
        parent_dn = ldb_dn_get_parent(tmp_ctx, dn);
        if (ldb_dn_compare_base(parent_dn, user_basedn) == 0) {
            um[um_index] = rdn_as_string(mem_ctx, dn);
            if (um[um_index] == NULL) {
                ret = ENOMEM;
                goto fail;
            }
            DEBUG(SSSDBG_TRACE_FUNC, "User member %s\n", um[um_index]);
            um_index++;
        } else if (ldb_dn_compare_base(parent_dn, group_basedn) == 0) {
            gm[gm_index] = rdn_as_string(mem_ctx, dn);
            if (gm[gm_index] == NULL) {
                ret = ENOMEM;
                goto fail;
            }
            if (parent_name && strcmp(gm[gm_index], parent_name) == 0) {
                DEBUG(SSSDBG_TRACE_FUNC,
                      "Skipping circular nesting for group %s\n",
                          gm[gm_index]);
                continue;
            }
            DEBUG(SSSDBG_TRACE_FUNC, "Group member %s\n", gm[gm_index]);
            gm_index++;
        } else {
            DEBUG(SSSDBG_OP_FAILURE, "Group member not a user nor group: %s\n",
                        ldb_dn_get_linearized(dn));
            ret = EIO;
            goto fail;
        }

        talloc_zfree(dn);
        talloc_zfree(parent_dn);
    }
    um[um_index] = NULL;
    gm[gm_index] = NULL;

    if (um_index > 0) {
        um = talloc_realloc(mem_ctx, um, const char *, um_index+1);
        if (!um) {
            ret = ENOMEM;
            goto fail;
        }
    } else {
        talloc_zfree(um);
    }

    if (gm_index > 0) {
        gm = talloc_realloc(mem_ctx, gm, const char *, gm_index+1);
        if (!gm) {
            ret = ENOMEM;
            goto fail;
        }
    } else {
        talloc_zfree(gm);
    }

    *user_members = um;
    if (group_members) *group_members = gm;
    if (num_group_members) *num_group_members = gm_index;
    talloc_zfree(tmp_ctx);
    return EOK;

fail:
    talloc_zfree(um);
    talloc_zfree(gm);
    talloc_zfree(tmp_ctx);
    return ret;
}

static int process_group(TALLOC_CTX *mem_ctx,
                         struct ldb_context *ldb,
                         struct ldb_message *msg,
                         struct sss_domain_info *domain,
                         const  char *parent_name,
                         struct group_info **info,
                         const char ***group_members,
                         int    *num_group_members)
{
    struct ldb_message_element *el;
    int ret, i, j;
    int count = 0;
    struct group_info *gi = NULL;
    const char **user_members;

    DEBUG(SSSDBG_TRACE_FUNC,
          "Found entry %s\n", ldb_dn_get_linearized(msg->dn));

    gi = talloc_zero(mem_ctx, struct group_info);
    if (!gi) {
        ret = ENOMEM;
        goto done;
    }

    /* mandatory data - name and gid */
    gi->name = talloc_strdup(gi,
                             ldb_msg_find_attr_as_string(msg,
                                                         SYSDB_NAME,
                                                         NULL));
    gi->gid = ldb_msg_find_attr_as_uint64(msg,
                                          SYSDB_GIDNUM, 0);
    if (gi->gid == 0 || gi->name == NULL) {
        DEBUG(SSSDBG_MINOR_FAILURE, "No name or no GID?\n");
        ret = EIO;
        goto done;
    }

    /* list members */
    el = ldb_msg_find_element(msg, SYSDB_MEMBER);
    if (el) {
        ret = parse_members(gi, ldb, domain, el,
                            parent_name,
                            &gi->user_members,
                            group_members, num_group_members);
        if (ret != EOK) {
            goto done;
        }
        if (gi->user_members == NULL) {
            count = 0;
        } else {
            for (count = 0; gi->user_members[count]; count++) ;
        }
    }
    el = ldb_msg_find_element(msg, SYSDB_GHOST);
    if (el) {
        ret = parse_members(gi, ldb, domain, el,
                            parent_name,
                            &user_members,
                            NULL, NULL);
        if (ret != EOK) {
            goto done;
        }

        if (user_members != NULL) {
            i = count;
            for (count = 0; user_members[count]; count++) ;
            gi->user_members = talloc_realloc(gi, gi->user_members,
                                              const char *,
                                              i + count + 1);
            if (gi->user_members == NULL) {
                ret = ENOMEM;
                goto done;
            }
            for (j = 0; j < count; j++, i++) {
                gi->user_members[i] = talloc_steal(gi->user_members,
                                                   user_members[j]);
            }
            gi->user_members[i] = NULL;

            talloc_zfree(user_members);
        }
    }

    /* list memberofs */
    el = ldb_msg_find_element(msg, SYSDB_MEMBEROF);
    if (el) {
        ret = parse_memberofs(ldb, el, gi);
        if (ret != EOK) {
            goto done;
        }
    }

    *info = gi;
    return EOK;
done:
    talloc_zfree(gi);
    return ret;
}

/*========Find info about a group and recursively about subgroups====== */

int group_show_recurse(TALLOC_CTX *mem_ctx,
                       struct sysdb_ctx *sysdb,
                       struct sss_domain_info *domain,
                       struct group_info *root,
                       struct group_info *parent,
                       const char **group_members,
                       const int  nmembers,
                       struct group_info ***up_members);

static int group_show_trim_memberof(TALLOC_CTX *mem_ctx,
                                    struct sss_domain_info *domain,
                                    const char *name,
                                    const char **memberofs,
                                    const char ***_direct);

int group_show(TALLOC_CTX *mem_ctx,
               struct sysdb_ctx *sysdb,
               struct sss_domain_info *domain,
               bool   recursive,
               const char *shortname,
               struct group_info **res)
{
    struct group_info *root;
    static const char *attrs[] = GROUP_SHOW_ATTRS;
    struct ldb_message *msg = NULL;
    const char **group_members = NULL;
    int nmembers = 0;
    char *sysdb_fqname = NULL;
    int ret;
    int i;

    sysdb_fqname = sss_create_internal_fqname(mem_ctx,
                                              shortname,
                                              domain->name);
    if (sysdb_fqname == NULL) {
        return ENOMEM;
    }

    /* First, search for the root group */
    ret = sysdb_search_group_by_name(mem_ctx, domain, sysdb_fqname, attrs,
                                     &msg);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Search failed: %s (%d)\n", strerror(ret), ret);
        goto done;
    }

    ret = process_group(mem_ctx, sysdb_ctx_get_ldb(sysdb),
                        msg, domain, NULL, &root,
                        &group_members, &nmembers);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Group processing failed: %s (%d)\n",
                   strerror(ret), ret);
        goto done;
    }

    if (!recursive) {
        if (group_members) {
            root->group_members = talloc_array(root,
                                               struct group_info *,
                                               nmembers+1);
            if (!root->group_members) {
                ret = ENOMEM;
                goto done;
            }
            for (i = 0; i < nmembers; i++) {
                root->group_members[i] = talloc_zero(root, struct group_info);
                if (!root->group_members[i]) {
                    ret = ENOMEM;
                    goto done;
                }
                root->group_members[i]->name = talloc_strdup(root,
                                                             group_members[i]);
                if (!root->group_members[i]->name) {
                    ret = ENOMEM;
                    goto done;
                }
            }
            root->group_members[nmembers] = NULL;
        }

        if (root->memberofs == NULL) {
            ret = EOK;
            goto done;
        }

        /* if not recursive, only show the direct parent */
        ret = group_show_trim_memberof(mem_ctx, domain, root->name,
                                       root->memberofs, &root->memberofs);
        goto done;
    }

    if (group_members == NULL) {
        ret = EOK;
        goto done;
    }

    ret = group_show_recurse(root, sysdb, domain, root, root,
                             group_members, nmembers,
                             &root->group_members);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Recursive search failed: %s (%d)\n", strerror(ret), ret);
        goto done;
    }

    ret = EOK;
done:
    if (ret == EOK) {
        *res = root;
    }
    return ret;
}

/*=========Nonrecursive search should only show direct parent========== */

static int group_show_trim_memberof(TALLOC_CTX *mem_ctx,
                                    struct sss_domain_info *domain,
                                    const char *name,
                                    const char **memberofs,
                                    const char ***_direct)
{
    struct ldb_dn *dn;
    char *filter;
    struct ldb_message **msgs;
    size_t count;
    const char **direct = NULL;
    int ndirect = 0;
    int ret;
    int i;

    dn = sysdb_group_dn(mem_ctx, domain, name);
    if (!dn) {
        return ENOMEM;
    }

    for (i = 0; memberofs[i]; i++) {

        filter = talloc_asprintf(mem_ctx, "(&(%s=%s)(%s=%s))",
                                 SYSDB_NAME, memberofs[i],
                                 SYSDB_MEMBER, ldb_dn_get_linearized(dn));
        if (!filter) {
            return ENOMEM;
        }

        ret = sysdb_search_groups(mem_ctx, domain,
                                  filter, NULL, &count, &msgs);
        /* ENOENT is OK, the group is just not a direct parent */
        if (ret != EOK && ret != ENOENT) {
            return ret;
        }

        if (count > 0) {
            name = ldb_msg_find_attr_as_string(msgs[0],
                                               SYSDB_NAME, NULL);
            if (!name) {
                DEBUG(SSSDBG_OP_FAILURE, "Entry %s has no Name Attribute ?!?\n",
                      ldb_dn_get_linearized(msgs[0]->dn));
                return EFAULT;
            }

            direct = talloc_realloc(mem_ctx, direct,
                                    const char *, ndirect + 2);
            if (!direct) {
                return ENOMEM;
            }

            direct[ndirect] = talloc_strdup(direct, name);
            if (!direct[ndirect]) {
                return ENOMEM;
            }

            direct[ndirect + 1] = NULL;
            ndirect++;
        }
    }

    *_direct = direct;
    return EOK;
}

/*==================Recursive search for nested groups================= */

int group_show_recurse(TALLOC_CTX *mem_ctx,
                       struct sysdb_ctx *sysdb,
                       struct sss_domain_info *domain,
                       struct group_info *root,
                       struct group_info *parent,
                       const char **group_members,
                       const int  nmembers,
                       struct group_info ***up_members)
{
    struct group_info **groups;
    static const char *attrs[] = GROUP_SHOW_ATTRS;
    struct ldb_message *msg;
    const char **new_group_members = NULL;
    int new_nmembers = 0;
    int ret;
    int i;

    groups = talloc_zero_array(root,
                               struct group_info *,
                               nmembers+1); /* trailing NULL */

    if (!group_members || !group_members[0]) {
        return ENOENT;
    }

    for (i = 0; i < nmembers; i++) {
        /* Skip circular groups */
        if (strcmp(group_members[i], parent->name) == 0) {
            continue;
        }

        ret = sysdb_search_group_by_name(mem_ctx, domain, group_members[i],
                                         attrs, &msg);
        if (ret) {
            DEBUG(SSSDBG_OP_FAILURE,
                  "Search failed: %s (%d)\n", strerror(ret), ret);
            return EIO;
        }

        ret = process_group(root, sysdb_ctx_get_ldb(sysdb),
                            msg, domain, parent->name,
                            &groups[i], &new_group_members, &new_nmembers);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "Group processing failed: %s (%d)\n",
                      strerror(ret), ret);
            return ret;
        }

        /* descend to another level */
        if (new_nmembers > 0) {
            ret = group_show_recurse(mem_ctx, sysdb, domain,
                                     root, groups[i],
                                     new_group_members, new_nmembers,
                                     &parent->group_members);
            if (ret != EOK) {
                DEBUG(SSSDBG_OP_FAILURE, "Recursive search failed: %s (%d)\n",
                          strerror(ret), ret);
                return ret;
            }
            talloc_zfree(new_group_members);
        }
    }

    *up_members = groups;
    return EOK;
}

/*==================Get info about MPG================================= */

static int group_show_mpg(TALLOC_CTX *mem_ctx,
                          struct sss_domain_info *domain,
                          const char *shortname,
                          struct group_info **res)
{
    const char *attrs[] = GROUP_SHOW_MPG_ATTRS;
    struct ldb_message *msg;
    struct group_info *info;
    int ret;
    char *sysdb_fqname;

    info = talloc_zero(mem_ctx, struct group_info);
    if (!info) {
        ret = ENOMEM;
        goto fail;
    }

    sysdb_fqname = sss_create_internal_fqname(mem_ctx,
                                              shortname,
                                              domain->name);
    if (sysdb_fqname == NULL) {
        return ENOMEM;
    }

    ret = sysdb_search_user_by_name(info, domain, sysdb_fqname, attrs, &msg);
    if (ret) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Search failed: %s (%d)\n", strerror(ret), ret);
        goto fail;
    }

    info->name = talloc_strdup(info,
                               ldb_msg_find_attr_as_string(msg,
                                                           SYSDB_NAME, NULL));
    info->gid = ldb_msg_find_attr_as_uint64(msg, SYSDB_UIDNUM, 0);
    if (info->gid == 0 || info->name == NULL) {
        DEBUG(SSSDBG_MINOR_FAILURE, "No name or no GID?\n");
        ret = EIO;
        goto fail;
    }
    info->mpg = true;

    *res = info;
    return EOK;

fail:
    talloc_zfree(info);
    return ret;
}

/*==================The main program=================================== */

static void print_group_info(struct group_info *g, unsigned level)
{
    int i;
    char padding[512];
    char fmt[8];

    snprintf(fmt, 8, "%%%ds", level*PADDING_SPACES);
    snprintf(padding, 512, fmt, "");

    PRINT("%1$s%2$sGroup: %3$s\n", padding,
          g->mpg ? _("Magic Private ") : "",
          g->name);
    PRINT("%1$sGID number: %2$d\n", padding, g->gid);

    PRINT("%1$sMember users: ", padding);
    if (g->user_members) {
        for (i=0; g->user_members[i]; ++i) {
            printf("%s%s", i>0 ? "," : "",
                           g->user_members[i]);
        }
    }
    PRINT("\n%1$sIs a member of: ", padding);
    if (g->memberofs) {
        for (i=0; g->memberofs[i]; ++i) {
            printf("%s%s", i>0 ? "," : "",
                           g->memberofs[i]);
        }
    }
    PRINT("\n%1$sMember groups: ", padding);
}

static void print_recursive(struct group_info **group_members, unsigned level)
{
    int i;

    if (group_members == NULL) {
        return;
    }

    level++;
    for (i=0; group_members[i]; ++i) {
        printf("\n");
        print_group_info(group_members[i], level);
        printf("\n");
        print_recursive(group_members[i]->group_members, level);
    }
}

int main(int argc, const char **argv)
{
    int ret = EXIT_SUCCESS;
    int pc_debug = SSSDBG_TOOLS_DEFAULT;
    bool pc_recursive = false;
    const char *pc_groupname = NULL;
    struct tools_ctx *tctx = NULL;
    struct group_info *root = NULL;
    int i;

    poptContext pc = NULL;
    struct poptOption long_options[] = {
        POPT_AUTOHELP
        { "debug", '\0', POPT_ARG_INT | POPT_ARGFLAG_DOC_HIDDEN, &pc_debug,
                    0, _("The debug level to run with"), NULL },
        { "recursive", 'R', POPT_ARG_NONE, NULL, 'r',
            _("Print indirect group members recursively"), NULL },
        POPT_TABLEEND
    };

    debug_prg_name = argv[0];

    ret = set_locale();
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "set_locale failed (%d): %s\n", ret, strerror(ret));
        ERROR("Error setting the locale\n");
        ret = EXIT_FAILURE;
        goto fini;
    }

    /* parse ops_ctx */
    pc = poptGetContext(NULL, argc, argv, long_options, 0);
    poptSetOtherOptionHelp(pc, "GROUPNAME");
    while ((ret = poptGetNextOpt(pc)) > 0) {
        switch (ret) {
            case 'r':
                pc_recursive = true;
                break;
        }
    }

    DEBUG_CLI_INIT(pc_debug);

    if (ret != -1) {
        BAD_POPT_PARAMS(pc, poptStrerror(ret), ret, fini);
    }

    pc_groupname = poptGetArg(pc);
    if (pc_groupname == NULL) {
        BAD_POPT_PARAMS(pc, _("Specify group to show\n"), ret, fini);
    }

    CHECK_ROOT(ret, debug_prg_name);

    ret = init_sss_tools(&tctx);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "init_sss_tools failed (%d): %s\n", ret, strerror(ret));
        if (ret == ENOENT) {
            ERROR("Error initializing the tools - no local domain\n");
        } else {
            ERROR("Error initializing the tools\n");
        }
        ret = EXIT_FAILURE;
        goto fini;
    }

    /* if the domain was not given as part of FQDN, default to local domain */
    ret = parse_name_domain(tctx, pc_groupname);
    if (ret != EOK) {
        ERROR("Invalid domain specified in FQDN\n");
        ret = EXIT_FAILURE;
        goto fini;
    }

    /* The search itself */
    ret = group_show(tctx, tctx->sysdb,
                     tctx->local, pc_recursive, tctx->octx->name, &root);
    /* Also show MPGs */
    if (ret == ENOENT) {
        ret = group_show_mpg(tctx, tctx->local, tctx->octx->name, &root);
    }

    /* Process result */
    if (ret) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "sysdb operation failed (%d)[%s]\n", ret, strerror(ret));
        switch (ret) {
            case ENOENT:
                ERROR("No such group in local domain. "
                      "Printing groups only allowed in local domain.\n");
                break;

            default:
                ERROR("Internal error. Could not print group.\n");
                break;
        }
        ret = EXIT_FAILURE;
        goto fini;
    }

    /* print the results */
    print_group_info(root, 0);
    if (pc_recursive) {
        printf("\n");
        print_recursive(root->group_members, 0);
    } else {
        if (root->group_members) {
            for (i=0; root->group_members[i]; ++i) {
                printf("%s%s", i>0 ? "," : "",
                       root->group_members[i]->name);
            }
        }
        printf("\n");
    }

fini:
    talloc_free(tctx);
    poptFreeContext(pc);
    exit(ret);
}
