/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2002 Tim J. Robbins.
 * All rights reserved.
 *
 * 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 AUTHOR 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 AUTHOR 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.
 */

/*
 * newgrp -- change to a new group
 */

#include <sys/types.h>

#include <err.h>
#include <errno.h>
#include <grp.h>
#include <limits.h>
#include <login_cap.h>
#include <paths.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void	 addgroup(const char *grpname);
static void	 doshell(void);
static int	 inarray(gid_t, const gid_t[], int);
static void	 loginshell(void);
static void	 restoregrps(void);
static void	 usage(void);

static struct passwd *pwd;
static uid_t euid;

extern char **environ;

/* Manipulate effective user ID. */
#define PRIV_START do {				\
		if (seteuid(euid) < 0)		\
			err(1, "seteuid");	\
	} while (0)
#define PRIV_END do {				\
		if (seteuid(getuid()) < 0)	\
			err(1, "seteuid");	\
	} while (0)

int
main(int argc, char *argv[])
{
	int ch, login;

	if ((euid = geteuid()) != 0)
		warnx("need root permissions to function properly, check setuid bit");
	if (seteuid(getuid()) < 0)
		err(1, "seteuid");

	if ((pwd = getpwuid(getuid())) == NULL)
		errx(1, "unknown user");

	login = 0;
	while ((ch = getopt(argc, argv, "-l")) != -1) {
		switch (ch) {
		case '-':		/* Obsolescent */
		case 'l':
			login = 1;
			break;
		default:
			usage();
		}
	}
	argc -= optind;
	argv += optind;

	switch (argc) {
	case 0:
		restoregrps();
		break;
	case 1:
		addgroup(*argv);
		break;
	default:
		usage();
	}

	if (seteuid(euid) < 0)
		err(1, "seteuid");
	if (setuid(getuid()) < 0)
		err(1, "setuid");

	if (login)
		loginshell();
	else
		doshell();

	/*NOTREACHED*/
	exit(1);
}

static void
usage(void)
{

	fprintf(stderr, "usage: newgrp [-l] [group]\n");
	exit(1);
}

static void
restoregrps(void)
{
	int initres, setres;

	PRIV_START;
	initres = initgroups(pwd->pw_name, pwd->pw_gid);
	setres = setgid(pwd->pw_gid);
	PRIV_END;

	if (initres < 0)
		warn("initgroups");
	if (setres < 0)
		warn("setgid");
}

static void
addgroup(const char *grpname)
{
	gid_t *grps;
	long lgid, ngrps_max;
	int dbmember, i, ngrps;
	gid_t egid;
	struct group *grp;
	char *ep, *pass, *cryptpw;
	char **p;

	egid = getegid();

	/* Try it as a group name, then a group id. */
	if ((grp = getgrnam(grpname)) == NULL)
		if ((lgid = strtol(grpname, &ep, 10)) <= 0 || *ep != '\0' ||
		    (grp = getgrgid((gid_t)lgid)) == NULL ) {
			warnx("%s: bad group name", grpname);
			return;
		}

	/*
	 * If the user is not a member of the requested group and the group
	 * has a password, prompt and check it.
	 */
	dbmember = 0;
	if (pwd->pw_gid == grp->gr_gid)
		dbmember = 1;
	for (p = grp->gr_mem; *p != NULL; p++)
		if (strcmp(*p, pwd->pw_name) == 0) {
			dbmember = 1;
			break;
		}
	if (!dbmember && *grp->gr_passwd != '\0' && getuid() != 0) {
		pass = getpass("Password:");
		if (pass == NULL)
			return;
		cryptpw = crypt(pass, grp->gr_passwd);
		if (cryptpw == NULL || strcmp(grp->gr_passwd, cryptpw) != 0) {
			fprintf(stderr, "Sorry\n");
			return;
		}
	}

	ngrps_max = sysconf(_SC_NGROUPS_MAX);
	if ((grps = malloc(sizeof(gid_t) * ngrps_max)) == NULL)
		err(1, "malloc");
	if ((ngrps = getgroups(ngrps_max, (gid_t *)grps)) < 0) {
		warn("getgroups");
		goto end;
	}

	/*
	 * Remove requested gid from supp. list if it exists and doesn't match
	 * our prior egid -- this exception is to avoid providing the user a
	 * means to get rid of a group that could be used for, e.g., negative
	 * permissions.
	 */
	if (grp->gr_gid != egid && inarray(grp->gr_gid, grps, ngrps)) {
		for (i = 0; i < ngrps; i++)
			if (grps[i] == grp->gr_gid)
				break;
		ngrps--;
		memmove(&grps[i], &grps[i + 1], (ngrps - i) * sizeof(gid_t));
		PRIV_START;
		if (setgroups(ngrps, (const gid_t *)grps) < 0) {
			PRIV_END;
			warn("setgroups");
			goto end;
		}
		PRIV_END;
	}

	PRIV_START;
	if (setgid(grp->gr_gid)) {
		PRIV_END;
		warn("setgid");
		goto end;
	}
	PRIV_END;

	/* Add old effective gid to supp. list if it does not exist. */
	if (!inarray(egid, grps, ngrps)) {
		if (ngrps == ngrps_max)
			warnx("too many groups");
		else {
			grps[ngrps++] = egid;
			PRIV_START;
			if (setgroups(ngrps, (const gid_t *)grps)) {
				PRIV_END;
				warn("setgroups");
				goto end;
			}
			PRIV_END;
		}
	}
end:
	free(grps);
}

static int
inarray(gid_t gid, const gid_t grps[], int ngrps)
{
	int i;

	for (i = 0; i < ngrps; i++)
		if (grps[i] == gid)
			return (1);
	return (0);
}

/*
 * Set the environment to what would be expected if the user logged in
 * again; this performs the same steps as su(1)'s -l option.
 */
static void
loginshell(void)
{
	char *args[2], **cleanenv, *term, *ticket;
	const char *shell;
	login_cap_t *lc;

	shell = pwd->pw_shell;
	if (*shell == '\0')
		shell = _PATH_BSHELL;
	if (chdir(pwd->pw_dir) < 0) {
		warn("%s", pwd->pw_dir);
		chdir("/");
	}

	term = getenv("TERM");
	ticket = getenv("KRBTKFILE");

	if ((cleanenv = calloc(20, sizeof(char *))) == NULL)
		err(1, "calloc");
	*cleanenv = NULL;
	environ = cleanenv;

	lc = login_getpwclass(pwd);
	setusercontext(lc, pwd, pwd->pw_uid,
	    LOGIN_SETPATH|LOGIN_SETUMASK|LOGIN_SETENV);
	login_close(lc);
	setenv("USER", pwd->pw_name, 1);
	setenv("SHELL", shell, 1);
	setenv("HOME", pwd->pw_dir, 1);
	if (term != NULL)
		setenv("TERM", term, 1);
	if (ticket != NULL)
		setenv("KRBTKFILE", ticket, 1);

	if (asprintf(args, "-%s", shell) < 0)
		err(1, "asprintf");
	args[1] = NULL;

	execv(shell, args);
	err(1, "%s", shell);
}

static void
doshell(void)
{
	const char *shell;

	shell = pwd->pw_shell;
	if (*shell == '\0')
		shell = _PATH_BSHELL;
	execl(shell, shell, (char *)NULL);
	err(1, "%s", shell);
}
