/*      $NetBSD: getgroupmembership.c,v 1.4 2008/04/28 20:22:59 martin Exp $    */

/*-
* Copyright (c) 2004-2005 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This code is derived from software contributed to The NetBSD Foundation
* by Luke Mewburn.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/

#include <sys/cdefs.h>
#if defined(LIBC_SCCS) && !defined(lint)
__RCSID("$NetBSD: getgroupmembership.c,v 1.4 2008/04/28 20:22:59 martin Exp $");
#endif /* LIBC_SCCS and not lint */

/*
* calculate group access list
*/

#include "namespace.h"
#include "reentrant.h"

#include <sys/param.h>

#include <assert.h>
#include <errno.h>
#include <grp.h>
#include <limits.h>
#include <nsswitch.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#ifdef HESIOD
#include <hesiod.h>
#endif

#include "gr_private.h"

#ifdef __weak_alias
__weak_alias(getgroupmembership,_getgroupmembership)
#endif

/*
* __gr_addgid
*      Add gid to the groups array (of maxgrp size) at the position
*      indicated by *groupc, unless it already exists or *groupc is
*      past &groups[maxgrp].
*      Returns 1 upon success (including duplicate suppression), 0 otherwise.
*/
static int
__gr_addgid(gid_t gid, gid_t *groups, int maxgrp, int *groupc)
{
       int     ret, dupc;

       _DIAGASSERT(groupc != NULL);
       _DIAGASSERT(groups != NULL);

                                               /* skip duplicates */
       for (dupc = 0; dupc < MIN(maxgrp, *groupc); dupc++) {
               if (groups[dupc] == gid)
                       return 1;
       }

       ret = 1;
       if (*groupc < maxgrp)                   /* add this gid */
               groups[*groupc] = gid;
       else
               ret = 0;
       (*groupc)++;
       return ret;
}


/*ARGSUSED*/
static int
_files_getgroupmembership(void *retval, void *cb_data, va_list ap)
{
       int             *result = va_arg(ap, int *);
       const char      *uname  = va_arg(ap, const char *);
       gid_t            agroup = va_arg(ap, gid_t);
       gid_t           *groups = va_arg(ap, gid_t *);
       int              maxgrp = va_arg(ap, int);
       int             *groupc = va_arg(ap, int *);

       struct __grstate_files  state;
       struct group            grp;
       char                    grpbuf[_GETGR_R_SIZE_MAX];
       int                     rv, i;

       _DIAGASSERT(result != NULL);
       _DIAGASSERT(uname != NULL);
       /* groups may be NULL if just sizing when invoked with maxgrp = 0 */
       _DIAGASSERT(groupc != NULL);

                                               /* install primary group */
       (void) __gr_addgid(agroup, groups, maxgrp, groupc);

       memset(&state, 0, sizeof(state));
       while (__grscan_files(&rv, &grp, grpbuf, sizeof(grpbuf), &state,
                               0, NULL, 0) == NS_SUCCESS) {
                                               /* scan members */
               for (i = 0; grp.gr_mem[i]; i++) {
                       if (strcmp(grp.gr_mem[i], uname) != 0)
                               continue;
                       if (! __gr_addgid(grp.gr_gid, groups, maxgrp, groupc))
                               *result = -1;
                       break;
               }
       }
       __grend_files(&state);
       return NS_NOTFOUND;
}


#ifdef HESIOD

/*ARGSUSED*/
static int
_dns_getgroupmembership(void *retval, void *cb_data, va_list ap)
{
       int             *result = va_arg(ap, int *);
       const char      *uname  = va_arg(ap, const char *);
       gid_t            agroup = va_arg(ap, gid_t);
       gid_t           *groups = va_arg(ap, gid_t *);
       int              maxgrp = va_arg(ap, int);
       int             *groupc = va_arg(ap, int *);

       struct __grstate_dns    state;
       struct group            grp;
       char                    grpbuf[_GETGR_R_SIZE_MAX];
       unsigned long           id;
       void                    *context;
       char                    **hp, *cp, *ep;
       int                     rv, i;

       _DIAGASSERT(result != NULL);
       _DIAGASSERT(uname != NULL);
       /* groups may be NULL if just sizing when invoked with maxgrp = 0 */
       _DIAGASSERT(groupc != NULL);

                                               /* install primary group */
       (void) __gr_addgid(agroup, groups, maxgrp, groupc);

       hp = NULL;
       rv = NS_NOTFOUND;

       if (hesiod_init(&context) == -1)                /* setup hesiod */
               return NS_UNAVAIL;

       hp = hesiod_resolve(context, uname, "grplist"); /* find grplist */
       if (hp == NULL) {
               if (errno != ENOENT) {                  /* wasn't "not found"*/
                       rv = NS_UNAVAIL;
                       goto dnsgroupmembers_out;
               }
                       /* grplist not found, fallback to _dns_grscan */
               memset(&state, 0, sizeof(state));
               while (__grscan_dns(&rv, &grp, grpbuf, sizeof(grpbuf), &state,
                                       0, NULL, 0) == NS_SUCCESS) {
                                                       /* scan members */
                       for (i = 0; grp.gr_mem[i]; i++) {
                               if (strcmp(grp.gr_mem[i], uname) != 0)
                                       continue;
                               if (! __gr_addgid(grp.gr_gid, groups, maxgrp,
                                   groupc))
                                       *result = -1;
                               break;
                       }
               }
               __grend_dns(&state);
               rv = NS_NOTFOUND;
               goto dnsgroupmembers_out;
       }

       if ((ep = strchr(hp[0], '\n')) != NULL)
               *ep = '\0';                             /* clear trailing \n */

       for (cp = hp[0]; *cp != '\0'; ) {               /* parse grplist */
               if ((cp = strchr(cp, ':')) == NULL)     /* skip grpname */
                       break;
               cp++;
               id = strtoul(cp, &ep, 10);              /* parse gid */
               if (id > GID_MAX || (*ep != ':' && *ep != '\0')) {
                       rv = NS_UNAVAIL;
                       goto dnsgroupmembers_out;
               }
               cp = ep;
               if (*cp == ':')
                       cp++;

                                                       /* add gid */
               if (! __gr_addgid((gid_t)id, groups, maxgrp, groupc))
                       *result = -1;
       }

       rv = NS_NOTFOUND;

dnsgroupmembers_out:
       if (hp)
               hesiod_free_list(context, hp);
       hesiod_end(context);
       return rv;
}

#endif /* HESIOD */


#ifdef YP

/*ARGSUSED*/
static int
_nis_getgroupmembership(void *retval, void *cb_data, va_list ap)
{
       int             *result = va_arg(ap, int *);
       const char      *uname  = va_arg(ap, const char *);
       gid_t            agroup = va_arg(ap, gid_t);
       gid_t           *groups = va_arg(ap, gid_t *);
       int              maxgrp = va_arg(ap, int);
       int             *groupc = va_arg(ap, int *);

       struct __grstate_nis    state;
       struct group            grp;
       char                    grpbuf[_GETGR_R_SIZE_MAX];
       int                     rv, i;

       _DIAGASSERT(result != NULL);
       _DIAGASSERT(uname != NULL);
       /* groups may be NULL if just sizing when invoked with maxgrp = 0 */
       _DIAGASSERT(groupc != NULL);

                                               /* install primary group */
       (void) __gr_addgid(agroup, groups, maxgrp, groupc);

       memset(&state, 0, sizeof(state));
       while (__grscan_nis(&rv, &grp, grpbuf, sizeof(grpbuf), &state,
                               0, NULL, 0) == NS_SUCCESS) {
                                               /* scan members */
               for (i = 0; grp.gr_mem[i]; i++) {
                       if (strcmp(grp.gr_mem[i], uname) != 0)
                               continue;
                       if (! __gr_addgid(grp.gr_gid, groups, maxgrp, groupc))
                               *result = -1;
                       break;
               }
       }
       __grend_nis(&state);

       return NS_NOTFOUND;
}

#endif /* YP */


#ifdef _GROUP_COMPAT

struct __compatggm {
       const char      *uname;         /* user to search for */
       gid_t           *groups;
       gid_t            agroup;
       int              maxgrp;
       int             *groupc;
};

static int
_compat_ggm_search(void *cookie, struct group **groupres)
{
       struct __compatggm      *cp;
       int                     rerror, crv;

       static const ns_dtab dtab[] = {
               NS_FILES_CB(__grbad_compat, "files")
               NS_DNS_CB(_dns_getgroupmembership, NULL)
               NS_NIS_CB(_nis_getgroupmembership, NULL)
               NS_COMPAT_CB(__grbad_compat, "compat")
               NS_NULL_CB
       };

       *groupres = NULL;       /* we don't care about this */
       cp = (struct __compatggm *)cookie;

       crv = nsdispatch(NULL, dtab,
           NSDB_GROUP_COMPAT, "getgroupmembership",
           __nsdefaultnis,
           &rerror, cp->uname, cp->agroup, cp->groups, cp->maxgrp, cp->groupc);

       if (crv == NS_SUCCESS)
               crv = NS_NOTFOUND;      /* indicate "no more +: entries" */

       return crv;
}

/* ARGSUSED */
static int
_compat_getgroupmembership(void *retval, void *cb_data, va_list ap)
{
       int             *result = va_arg(ap, int *);
       const char      *uname  = va_arg(ap, const char *);
       gid_t            agroup = va_arg(ap, gid_t);
       gid_t           *groups = va_arg(ap, gid_t *);
       int              maxgrp = va_arg(ap, int);
       int             *groupc = va_arg(ap, int *);

       struct __grstate_compat state;
       struct __compatggm      ggmstate;
       struct group            grp;
       char                    grpbuf[_GETGR_R_SIZE_MAX];
       int                     rv, i;

       _DIAGASSERT(result != NULL);
       _DIAGASSERT(uname != NULL);
       /* groups may be NULL if just sizing when invoked with maxgrp = 0 */
       _DIAGASSERT(groupc != NULL);

                                               /* install primary group */
       (void) __gr_addgid(agroup, groups, maxgrp, groupc);

       memset(&state, 0, sizeof(state));
       memset(&ggmstate, 0, sizeof(ggmstate));
       ggmstate.uname = uname;
       ggmstate.groups = groups;
       ggmstate.agroup = agroup;
       ggmstate.maxgrp = maxgrp;
       ggmstate.groupc = groupc;

       while (__grscan_compat(&rv, &grp, grpbuf, sizeof(grpbuf), &state,
                               0, NULL, 0, _compat_ggm_search, &ggmstate)
               == NS_SUCCESS) {
                                               /* scan members */
               for (i = 0; grp.gr_mem[i]; i++) {
                       if (strcmp(grp.gr_mem[i], uname) != 0)
                               continue;
                       if (! __gr_addgid(grp.gr_gid, groups, maxgrp, groupc))
                               *result = -1;
                       break;
               }
       }

       __grend_compat(&state);
       return NS_NOTFOUND;
}

#endif  /* _GROUP_COMPAT */


int
getgroupmembership(const char *uname, gid_t agroup,
   gid_t *groups, int maxgrp, int *groupc)
{
       int     rerror;

       static const ns_dtab dtab[] = {
               NS_FILES_CB(_files_getgroupmembership, NULL)
               NS_DNS_CB(_dns_getgroupmembership, NULL)
               NS_NIS_CB(_nis_getgroupmembership, NULL)
               NS_COMPAT_CB(_compat_getgroupmembership, NULL)
               NS_NULL_CB
       };

       _DIAGASSERT(uname != NULL);
       /* groups may be NULL if just sizing when invoked with maxgrp = 0 */
       _DIAGASSERT(groupc != NULL);

       *groupc = 0;

       mutex_lock(&__grmutex);
                       /*
                        * Call each backend.
                        * For compatibility with getgrent(3) semantics,
                        * a backend should return NS_NOTFOUND even upon
                        * completion, to allow result merging to occur.
                        */
       (void) nsdispatch(NULL, dtab, NSDB_GROUP, "getgroupmembership",
           __nsdefaultcompat,
           &rerror, uname, agroup, groups, maxgrp, groupc);
       mutex_unlock(&__grmutex);

       if (*groupc > maxgrp)                   /* too many groups found */
               return -1;
       else
               return 0;
}