/*****************************************************************************\
 *  groups.c - Functions to gather group membership information
 *             These functions utilize a cache for performance reasons
 *****************************************************************************
 *  Copyright (C) 2010 Lawrence Livermore National Security.
 *  Produced at Lawrence Livermore National Laboratory (cf, DISCLAIMER).
 *  Written by Morris Jette <jette1@llnl.gov> et. al.
 *  CODE-OCEC-09-009. All rights reserved.
 *
 *  This file is part of Slurm, a resource management program.
 *  For details, see <https://slurm.schedmd.com/>.
 *  Please also read the included file: DISCLAIMER.
 *
 *  Slurm 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 2 of the License, or (at your option)
 *  any later version.
 *
 *  In addition, as a special exception, the copyright holders give permission
 *  to link the code of portions of this program with the OpenSSL library under
 *  certain conditions as described in each individual source file, and
 *  distribute linked combinations including the two. You must obey the GNU
 *  General Public License in all respects for all of the code used other than
 *  OpenSSL. If you modify file(s) with this exception, you may extend this
 *  exception to your version of the file(s), but you are not obligated to do
 *  so. If you do not wish to do so, delete this exception statement from your
 *  version.  If you delete this exception statement from all source files in
 *  the program, then also delete it here.
 *
 *  Slurm 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 Slurm; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
\*****************************************************************************/

#include "config.h"

/* needed for getgrent_r */
#define _GNU_SOURCE

#include <grp.h>
#include <pthread.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "src/common/list.h"
#include "src/common/log.h"
#include "src/common/uid.h"
#include "src/common/xassert.h"
#include "src/common/xmalloc.h"
#include "src/common/xstring.h"

#include "slurm/slurm_errno.h"

#include "groups.h"

/* Pathname of group file record for checking update times */
#ifndef GROUP_FILE
#define GROUP_FILE	"/etc/group"
#endif

#define _DEBUG 0

static uid_t *_get_group_members(char *group_name, int *uid_cnt);
static void   _cache_del_func(void *x);
static uid_t *_get_group_cache(char *group_name, int *uid_cnt);
static void   _log_group_members(char *group_name,
				 uid_t *group_uids,
				 int uid_cnt);
static void   _put_group_cache(char *group_name, void *group_uids, int uid_cnt);

static List group_cache_list = NULL;
static pthread_mutex_t group_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
struct group_cache_rec {
	char *group_name;
	int uid_cnt;
	uid_t *group_uids;
};

/*
 * _uid_cmp
 */
static int _uid_cmp(const void *x, const void *y)
{
	uid_t a;
	uid_t b;

	a = *(uid_t *)x;
	b = *(uid_t *)y;

	/* Sort in increasing order so that the 0 is at the beginning */
	if (a < b)
		return -1;
	if (a > b)
		return 1;
	return 0;
}

/*
 * _remove_duplicate_uids()
 */
static void _remove_duplicate_uids(uid_t **u, int *u_cnt)
{
	int j = 0;
	uid_t *v;
	uid_t cur;

	xassert(u);
	xassert(u_cnt);
	if ((!*u) || (!*u_cnt))
		return;

	v = xcalloc(*u_cnt, sizeof(uid_t));
	qsort(*u, *u_cnt, sizeof(uid_t), _uid_cmp);

	cur = (*u)[0];
	for (int i = 0; i < *u_cnt; i++) {
		if ((*u)[i] == cur)
			continue;
		v[j++] = cur;
		cur = (*u)[i];
	}
	v[j++] = cur;

	xfree(*u);
	*u = v;
	*u_cnt = j;
}

extern uid_t *get_groups_members(char *group_names, int *user_cnt)
{
	uid_t *group_uids = NULL;
	char *tmp_names = NULL, *name_ptr = NULL, *one_group_name = NULL;

	*user_cnt = 0;
	if (group_names == NULL)
		return NULL;

	tmp_names = xstrdup(group_names);
	one_group_name = strtok_r(tmp_names, ",", &name_ptr);
	while (one_group_name) {
		int tmp_uid_cnt = 0;
		uid_t *temp_uids =  _get_group_members(one_group_name,
						       &tmp_uid_cnt);
		if (!tmp_uid_cnt) {
			xfree(temp_uids);
		} else if (!group_uids) {
			group_uids = temp_uids;
			*user_cnt = tmp_uid_cnt;
		} else {
			/* concatenate the uid_lists and free the new one */
			xrealloc(group_uids,
				 sizeof(uid_t) * (*user_cnt + tmp_uid_cnt));
			for (int i = 0; i < tmp_uid_cnt; i++)
				group_uids[(*user_cnt)++] = temp_uids[i];
			xfree(temp_uids);
		}
		one_group_name = strtok_r(NULL, ",", &name_ptr);
	}
	xfree(tmp_names);

	_remove_duplicate_uids(&group_uids, user_cnt);
	return group_uids;
}

/*
 * _get_group_members - identify the users in a given group name
 * IN group_name - a single group name
 * OUT uid_cnt - pointer to fill the size of returned array
 * RET list of its UIDs or NULL on error
 * NOTE: The caller must xfree non-NULL return values
 */
static uid_t *_get_group_members(char *group_name, int *uid_cnt)
{
	char *grp_buffer = NULL;
  	struct group grp,  *grp_result = NULL;
	struct passwd *pwd_result = NULL;
	uid_t *group_uids = NULL, my_uid;
	gid_t my_gid;
	int buflen = PW_BUF_SIZE, i, res, group_uids_size = 0;
#if defined (__APPLE__)
#else
	char pw_buffer[PW_BUF_SIZE];
	struct passwd pw;
#endif

	*uid_cnt = 0;

	group_uids = _get_group_cache(group_name, uid_cnt);
	if (*uid_cnt) {
		/* We found in cache */
		_log_group_members(group_name, group_uids, *uid_cnt);
		return group_uids;
	}

#if defined(_SC_GETGR_R_SIZE_MAX)
	i = sysconf(_SC_GETGR_R_SIZE_MAX);
	buflen = MAX(buflen, i);
#endif
	grp_buffer = xmalloc(buflen);
	while (1) {
		slurm_seterrno(0);
		res = getgrnam_r(group_name, &grp, grp_buffer, buflen,
				 &grp_result);

		/* We need to check for !grp_result, since it appears some
		 * versions of this function do not return an error on
		 * failure.
		 */
		if (res != 0 || !grp_result) {
			if (errno == ERANGE) {
				buflen *= 2;
				xrealloc(grp_buffer, buflen);
				continue;
			}
			error("%s: Could not find configured group %s",
			      __func__, group_name);
			xfree(grp_buffer);
			return NULL;
		}
		break;
	}
	my_gid = grp_result->gr_gid;

	/* Get the members from the getgrnam_r() call.
	 */
	for (i = 0; grp_result->gr_mem[i]; i++) {

		if (uid_from_string(grp_result->gr_mem[i],
				    &my_uid) < 0) {
			continue;
		}
		if (my_uid == 0)
			continue;
		if (group_uids_size < (*uid_cnt + 1)) {
			group_uids_size += 100;
			xrealloc(group_uids, (sizeof(uid_t) * group_uids_size));
		}
		group_uids[(*uid_cnt)++] = my_uid;
	}

	/* Note that in environments where user/group enumeration has
	 * been disabled (typically necessary for large user/group
	 * databases), the rest of this function essentially does
	 * nothing.  */

#if defined (__APPLE__)
	setgrent();
	while (1) {
		if ((grp_result = getgrent()) == NULL)
			break;
#else
	setgrent();
	while (1) {
		/* MH-CEA workaround to handle different group entries with
		 * the same gid
		 */
		slurm_seterrno(0);
		res = getgrent_r(&grp, grp_buffer, buflen, &grp_result);
		if (res != 0 || grp_result == NULL) {
			/* FreeBSD returns 0 and sets the grp_result to NULL
			 * unlike linux which returns ENOENT.
			 */
			if (errno == ERANGE) {
				buflen *= 2;
				xrealloc(grp_buffer, buflen);
				continue;
			}
			break;
		}
#endif
	        if (grp_result->gr_gid == my_gid) {
			if (xstrcmp(grp_result->gr_name, group_name)) {
				debug("including members of group '%s' as it "
				      "corresponds to the same gid as group"
				      " '%s'",grp_result->gr_name,group_name);
			}

		        for (i=0; grp_result->gr_mem[i]; i++) {
				if (uid_from_string(grp_result->gr_mem[i],
						    &my_uid) < 0) {
					/* Group member without valid login */
					continue;
				}
				if (my_uid == 0)
					continue;
				if (group_uids_size < (*uid_cnt + 1)) {
					group_uids_size += 100;
					xrealloc(group_uids, (sizeof(uid_t) *
							      group_uids_size));
				}
				group_uids[(*uid_cnt)++] = my_uid;
			}
		}
	}
	endgrent();
	setpwent();
#if defined (__APPLE__)
	while ((pwd_result = getpwent()) != NULL) {
#else
	while (!getpwent_r(&pw, pw_buffer, PW_BUF_SIZE, &pwd_result)) {
#endif
		/* At eof FreeBSD returns 0 unlike Linux
		 * which returns ENOENT.
		 */
		if (pwd_result == NULL)
			break;
 		if (pwd_result->pw_gid != my_gid)
			continue;
		if (group_uids_size < (*uid_cnt + 1)) {
			group_uids_size += 100;
			xrealloc(group_uids, (sizeof(uid_t) * group_uids_size));
		}
		group_uids[(*uid_cnt)++] = pwd_result->pw_uid;
	}
	endpwent();
	xfree(grp_buffer);
	_put_group_cache(group_name, group_uids, *uid_cnt);
	_log_group_members(group_name, group_uids, *uid_cnt);

	if (!(*uid_cnt))
		xfree(group_uids);
	return group_uids;
}

/* Delete our group/uid cache */
extern void clear_group_cache(void)
{
	slurm_mutex_lock(&group_cache_mutex);
	FREE_NULL_LIST(group_cache_list);
	slurm_mutex_unlock(&group_cache_mutex);
}

/* get_group_tlm - return the time of last modification for the GROUP_FILE */
extern time_t get_group_tlm(void)
{
	struct stat stat_buf;

	if (stat(GROUP_FILE, &stat_buf)) {
		error("Can't stat file %s %m", GROUP_FILE);
		return (time_t) 0;
	}
	return stat_buf.st_mtime;
}


/* Get a record from our group/uid cache.
 * Return NULL if not found. */
static uid_t *_get_group_cache(char *group_name, int *uid_cnt)
{
	list_itr_t *iter;
	struct group_cache_rec *cache_rec;
	uid_t *group_uids = NULL;
	int sz;

	*uid_cnt = 0;

	slurm_mutex_lock(&group_cache_mutex);
	if (!group_cache_list) {
		slurm_mutex_unlock(&group_cache_mutex);
		return NULL;
	}

	iter = list_iterator_create(group_cache_list);
	while ((cache_rec = list_next(iter))) {
		if (xstrcmp(group_name, cache_rec->group_name))
			continue;
		sz = sizeof(uid_t) * cache_rec->uid_cnt;
		group_uids = xmalloc(sz);
		memcpy(group_uids, cache_rec->group_uids, sz);
		*uid_cnt = cache_rec->uid_cnt;
		break;
	}
	list_iterator_destroy(iter);
	slurm_mutex_unlock(&group_cache_mutex);
	return group_uids;
}

/* Delete a record from the group/uid cache, used by list functions */
static void _cache_del_func(void *x)
{
	struct group_cache_rec *cache_rec;

	cache_rec = (struct group_cache_rec *) x;
	xfree(cache_rec->group_name);
	xfree(cache_rec->group_uids);
	xfree(cache_rec);
}

/* Put a record on our group/uid cache */
static void _put_group_cache(char *group_name, void *group_uids, int uid_cnt)
{
	struct group_cache_rec *cache_rec;
	int sz;

	slurm_mutex_lock(&group_cache_mutex);
	if (!group_cache_list) {
		group_cache_list = list_create(_cache_del_func);
	}

	sz = sizeof(uid_t) * (uid_cnt);
	cache_rec = xmalloc(sizeof(struct group_cache_rec));
	cache_rec->group_name = xstrdup(group_name);
	cache_rec->uid_cnt    = uid_cnt;
	cache_rec->group_uids = xmalloc(sizeof(uid_t) + sz);
	if (uid_cnt > 0)
		memcpy(cache_rec->group_uids, group_uids, sz);
	list_append(group_cache_list, cache_rec);
	slurm_mutex_unlock(&group_cache_mutex);
}

static void _log_group_members(char *group_name, uid_t *group_uids, int uid_cnt)
{
#if _DEBUG
	if ((!group_uids) || (!uid_cnt)) {
		info("Group %s has no users", group_name);
		return;
	}

	info("Group %s contains uids:", group_name);
	for (int i = 0; i < uid_cnt; i++)
		info("  %u", group_uids[i]);
#endif
}
