/************************************************************************
 *
 * newrole
 *
 * SYNOPSIS:
 *
 * This program allows a user to change their SELinux RBAC role and/or
 * SELinux TE type (domain) in a manner similar to the way the traditional
 * UNIX su program allows a user to change their identity.
 *
 * USAGE:
 *
 * newrole [ -r role ] [ -t type ] [ -l level ] [ -V ] [ -- args ]
 *
 * BUILD OPTIONS:
 *
 * option USE_PAM:
 *
 * Set the USE_PAM constant if you want to authenticate users via PAM.
 * If USE_PAM is not set, users will be authenticated via direct
 * access to the shadow password file.
 *
 * If you decide to use PAM must be told how to handle newrole.  A
 * good rule-of-thumb might be to tell PAM to handle newrole in the
 * same way it handles su, except that you should remove the pam_rootok.so
 * entry so that even root must re-authenticate to change roles. 
 *
 * If you choose not to use PAM, make sure you have a shadow passwd file
 * in /etc/shadow.  You can use a symlink if your shadow passwd file
 * lives in another directory.  Example:
 *   su
 *   cd /etc
 *   ln -s /etc/auth/shadow shadow
 *
 * If you decide not to use PAM, you will also have to make newrole
 * setuid root, so that it can read the shadow passwd file.
 * 
 *
 * Authors:
 *      Anthony Colatrella
 *	Tim Fraser
 *	Steve Grubb <sgrubb@redhat.com>
 *	Darrel Goeddel <DGoeddel@trustedcs.com>
 *	Michael Thompson <mcthomps@us.ibm.com>
 *	Dan Walsh <dwalsh@redhat.com>
 *
 *************************************************************************/

#define _GNU_SOURCE

#if defined(AUDIT_LOG_PRIV) && !defined(USE_AUDIT)
#error AUDIT_LOG_PRIV needs the USE_AUDIT option
#endif
#if defined(NAMESPACE_PRIV) && !defined(USE_PAM)
#error NAMESPACE_PRIV needs the USE_PAM option
#endif

#include <stdio.h>
#include <stdlib.h>		/* for malloc(), realloc(), free() */
#include <pwd.h>		/* for getpwuid() */
#include <ctype.h>
#include <sys/types.h>		/* to make getuid() and getpwuid() happy */
#include <sys/wait.h>		/* for wait() */
#include <getopt.h>		/* for getopt_long() form of getopt() */
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <selinux/selinux.h>	/* for is_selinux_enabled() */
#include <selinux/context.h>	/* for context-mangling functions */
#include <selinux/get_default_type.h>
#include <selinux/get_context_list.h>	/* for SELINUX_DEFAULTUSER */
#include <signal.h>
#include <unistd.h>		/* for getuid(), exit(), getopt() */
#ifdef USE_AUDIT
#include <libaudit.h>
#endif
#if defined(AUDIT_LOG_PRIV) || defined(NAMESPACE_PRIV)
#include <sys/prctl.h>
#include <cap-ng.h>
#endif
#ifdef USE_NLS
#include <locale.h>		/* for setlocale() */
#include <libintl.h>		/* for gettext() */
#define _(msgid) gettext (msgid)
#else
#define _(msgid) (msgid)
#endif
#ifndef PACKAGE
#define PACKAGE "policycoreutils"	/* the name of this package lang translation */
#endif

#define TRUE 1
#define FALSE 0

/* USAGE_STRING describes the command-line args of this program. */
#define USAGE_STRING "USAGE: newrole [ -r role ] [ -t type ] [ -l level ] [ -p ] [ -V ] [ -- args ]"

#ifdef USE_PAM
#define PAM_SERVICE_CONFIG "/etc/selinux/newrole_pam.conf"
#endif

#define DEFAULT_PATH "/usr/bin:/bin"

extern char **environ;

/**
 * Construct from the current range and specified desired level a resulting
 * range. If the specified level is a range, return that. If it is not, then
 * construct a range with level as the sensitivity and clearance of the current
 * context.
 *
 * newlevel - the level specified on the command line
 * range    - the range in the current context
 *
 * Returns malloc'd memory
 */
static char *build_new_range(const char *newlevel, const char *range)
{
	char *newrangep = NULL;
	const char *tmpptr;
	size_t len;

	/* a missing or empty string */
	if (!range || !strlen(range) || !newlevel || !strlen(newlevel))
		return NULL;

	/* if the newlevel is actually a range - just use that */
	if (strchr(newlevel, '-')) {
		newrangep = strdup(newlevel);
		return newrangep;
	}

	/* look for MLS range in current context */
	tmpptr = strchr(range, '-');
	if (tmpptr) {
		/* we are inserting into a ranged MLS context */
		len = strlen(newlevel) + 1 + strlen(tmpptr + 1) + 1;
		newrangep = (char *)malloc(len);
		if (!newrangep)
			return NULL;
		snprintf(newrangep, len, "%s-%s", newlevel, tmpptr + 1);
	} else {
		/* we are inserting into a currently non-ranged MLS context */
		if (!strcmp(newlevel, range)) {
			newrangep = strdup(range);
		} else {
			len = strlen(newlevel) + 1 + strlen(range) + 1;
			newrangep = (char *)malloc(len);
			if (!newrangep)
				return NULL;
			snprintf(newrangep, len, "%s-%s", newlevel, range);
		}
	}

	return newrangep;
}

#ifdef USE_PAM

/************************************************************************
 *
 * All PAM code goes in this section.
 *
 ************************************************************************/
#include <security/pam_appl.h>	/* for PAM functions */
#include <security/pam_misc.h>	/* for misc_conv PAM utility function */

static const char *service_name = "newrole";

/* authenticate_via_pam()
 *
 * in:     pw - struct containing data from our user's line in 
 *                         the passwd file.
 * out:    nothing
 * return: value   condition
 *         -----   ---------
 *           1     PAM thinks that the user authenticated themselves properly
 *           0     otherwise
 *
 * This function uses PAM to authenticate the user running this
 * program.  This is the only function in this program that makes PAM
 * calls.
 */
static int authenticate_via_pam(const char *ttyn, pam_handle_t * pam_handle)
{

	int result = 0;		/* set to 0 (not authenticated) by default */
	int pam_rc;		/* pam return code */
	const char *tty_name;

	if (ttyn) {
		if (strncmp(ttyn, "/dev/", 5) == 0)
			tty_name = ttyn + 5;
		else
			tty_name = ttyn;

		pam_rc = pam_set_item(pam_handle, PAM_TTY, tty_name);
		if (pam_rc != PAM_SUCCESS) {
			fprintf(stderr, _("failed to set PAM_TTY\n"));
			goto out;
		}
	}

	/* Ask PAM to authenticate the user running this program */
	pam_rc = pam_authenticate(pam_handle, 0);
	if (pam_rc != PAM_SUCCESS) {
		goto out;
	}

	/* Ask PAM to verify acct_mgmt */
	pam_rc = pam_acct_mgmt(pam_handle, 0);
	if (pam_rc == PAM_SUCCESS) {
		result = 1;	/* user authenticated OK! */
	}

      out:
	return result;
}				/* authenticate_via_pam() */

#include "hashtab.h"

static int free_hashtab_entry(hashtab_key_t key, hashtab_datum_t d,
			      void *args __attribute__ ((unused)))
{
	free(key);
	free(d);
	return 0;
}

static unsigned int reqsymhash(hashtab_t h, const_hashtab_key_t key)
{
	const char *p;
	size_t size;
	unsigned int val;

	val = 0;
	size = strlen(key);
	for (p = key; ((size_t) (p - key)) < size; p++)
		val =
		    (val << 4 | (val >> (8 * sizeof(unsigned int) - 4))) ^ (*p);
	return val & (h->size - 1);
}

static int reqsymcmp(hashtab_t h
		     __attribute__ ((unused)), const_hashtab_key_t key1,
		     const_hashtab_key_t key2)
{
	return strcmp(key1, key2);
}

static hashtab_t app_service_names = NULL;
#define PAM_SERVICE_SLOTS 64

static int process_pam_config(FILE * cfg)
{
	const char *config_file_path = PAM_SERVICE_CONFIG;
	char *line_buf = NULL;
	unsigned long lineno = 0;
	size_t len = 0;
	char *app = NULL;
	char *service = NULL;
	int ret;

	while (getline(&line_buf, &len, cfg) > 0) {
		char *buffer = line_buf;
		lineno++;
		while (isspace(*buffer))
			buffer++;
		if (buffer[0] == '#')
			continue;
		if (buffer[0] == '\n' || buffer[0] == '\0')
			continue;

		app = service = NULL;
		ret = sscanf(buffer, "%ms %ms\n", &app, &service);
		if (ret < 2 || !app || !service)
			goto err;

		ret = hashtab_insert(app_service_names, app, service);
		if (ret == HASHTAB_OVERFLOW) {
			fprintf(stderr,
				_
				("newrole: service name configuration hashtable overflow\n"));
			goto err;
		}
	}

	free(line_buf);
	return 0;
      err:
	free(app);
	free(service);
	fprintf(stderr, _("newrole:  %s:  error on line %lu.\n"),
		config_file_path, lineno);
	free(line_buf);
	return -1;
}

/* 
 *  Read config file ignoring comment lines.
 *  Files specified one per line executable with a corresponding
 *  pam service name.
 */
static int read_pam_config(void)
{
	const char *config_file_path = PAM_SERVICE_CONFIG;
	FILE *cfg = NULL;
	cfg = fopen(config_file_path, "r");
	if (!cfg)
		return 0;	/* This configuration is optional. */
	app_service_names =
	    hashtab_create(reqsymhash, reqsymcmp, PAM_SERVICE_SLOTS);
	if (!app_service_names)
		goto err;
	if (process_pam_config(cfg))
		goto err;
	fclose(cfg);
	return 0;
      err:
	fclose(cfg);
	return -1;
}

#else				/* else !USE_PAM */

/************************************************************************
 *
 * All shadow passwd code goes in this section.
 *
 ************************************************************************/
#include <shadow.h>		/* for shadow passwd functions */
#include <string.h>		/* for strlen(), memset() */

#define PASSWORD_PROMPT _("Password:")	/* prompt for getpass() */

static void memzero(void *ptr, size_t size)
{
	volatile unsigned char * volatile p = ptr;
	while (size--) {
		*p++ = '\0';
	}
}

/* authenticate_via_shadow_passwd()
 *
 * in:     uname - the calling user's user name
 * out:    nothing
 * return: value   condition
 *         -----   ---------
 *           1     user authenticated themselves properly according to the
 *                 shadow passwd file.
 *           0     otherwise
 *
 * This function uses the shadow passwd file to thenticate the user running
 * this program.
 */
static int authenticate_via_shadow_passwd(const char *uname)
{
	struct spwd *p_shadow_line;
	char *unencrypted_password_s;
	char *encrypted_password_s;
	int ret;

	setspent();
	p_shadow_line = getspnam(uname);
	endspent();
	if (!(p_shadow_line)) {
		fprintf(stderr, _("Cannot find your entry in the shadow "
				  "passwd file.\n"));
		return 0;
	}

	/* Ask user to input unencrypted password */
	if (!(unencrypted_password_s = getpass(PASSWORD_PROMPT))) {
		fprintf(stderr, _("getpass cannot open /dev/tty\n"));
		return 0;
	}

	/* Use crypt() to encrypt user's input password. */
	errno = 0;
	encrypted_password_s = crypt(unencrypted_password_s,
				     p_shadow_line->sp_pwdp);
	memzero(unencrypted_password_s, strlen(unencrypted_password_s));
	if (errno || !encrypted_password_s) {
		fprintf(stderr, _("Cannot encrypt password.\n"));
		return 0;
	}

	ret = !strcmp(encrypted_password_s, p_shadow_line->sp_pwdp);
	memzero(encrypted_password_s, strlen(encrypted_password_s));
	return ret;
}
#endif				/* if/else USE_PAM */

/**
 * This function checks to see if the shell is known in /etc/shells.
 * If so, it returns 1. On error or illegal shell, it returns 0.
 */
static int verify_shell(const char *shell_name)
{
	int found = 0;
	const char *buf;

	if (!(shell_name && shell_name[0]))
		return found;

	while ((buf = getusershell()) != NULL) {
		/* ignore comments */
		if (*buf == '#')
			continue;

		/* check the shell skipping newline char */
		if (!strcmp(shell_name, buf)) {
			found = 1;
			break;
		}
	}
	endusershell();
	return found;
}

/**
 * Determine the Linux user identity to re-authenticate.
 * If supported and set, use the login uid, as this should be more stable.
 * Otherwise, use the real uid.
 *
 * This function assigns malloc'd memory into the pw_copy struct.
 * Returns zero on success, non-zero otherwise
 */
static int extract_pw_data(struct passwd *pw_copy)
{
	uid_t uid;
	struct passwd *pw;

#ifdef USE_AUDIT
	uid = audit_getloginuid();
	if (uid == (uid_t) - 1)
		uid = getuid();
#else
	uid = getuid();
#endif

	setpwent();
	pw = getpwuid(uid);
	endpwent();
	if (!(pw && pw->pw_name && pw->pw_name[0] && pw->pw_shell
	      && pw->pw_shell[0] && pw->pw_dir && pw->pw_dir[0])) {
		fprintf(stderr,
			_("cannot find valid entry in the passwd file.\n"));
		return -1;
	}

	*pw_copy = *pw;
	pw = pw_copy;
	pw->pw_name = strdup(pw->pw_name);
	pw->pw_dir = strdup(pw->pw_dir);
	pw->pw_shell = strdup(pw->pw_shell);

	if (!(pw->pw_name && pw->pw_dir && pw->pw_shell)) {
		fprintf(stderr, _("Out of memory!\n"));
		goto out_free;
	}

	if (verify_shell(pw->pw_shell) == 0) {
		fprintf(stderr, _("Error!  Shell is not valid.\n"));
		goto out_free;
	}
	return 0;

      out_free:
	free(pw->pw_name);
	free(pw->pw_dir);
	free(pw->pw_shell);
	pw->pw_name = NULL;
	pw->pw_dir = NULL;
	pw->pw_shell = NULL;
	return -1;
}

/**
 * Either restore the original environment, or set up a minimal one.
 *
 * The minimal environment contains:
 * TERM, DISPLAY and XAUTHORITY - if they are set, preserve values
 * HOME, SHELL, USER and LOGNAME - set to contents of /etc/passwd
 * PATH - set to default value DEFAULT_PATH
 *
 * Returns zero on success, non-zero otherwise
 */
static int restore_environment(int preserve_environment,
			       char **old_environ, const struct passwd *pw)
{
	char const *term_env;
	char const *display_env;
	char const *xauthority_env;
	char *term = NULL;	/* temporary container */
	char *display = NULL;	/* temporary container */
	char *xauthority = NULL;	/* temporary container */
	int rc;

	environ = old_environ;

	if (preserve_environment)
		return 0;

	term_env = getenv("TERM");
	display_env = getenv("DISPLAY");
	xauthority_env = getenv("XAUTHORITY");

	/* Save the variable values we want */
	if (term_env)
		term = strdup(term_env);
	if (display_env)
		display = strdup(display_env);
	if (xauthority_env)
		xauthority = strdup(xauthority_env);
	if ((term_env && !term) || (display_env && !display) ||
	    (xauthority_env && !xauthority)) {
		rc = -1;
		goto out;
	}

	/* Construct a new environment */
	if ((rc = clearenv())) {
		fprintf(stderr, _("Unable to clear environment\n"));
		goto out;
	}

	/* Restore that which we saved */
	if (term)
		rc |= setenv("TERM", term, 1);
	if (display)
		rc |= setenv("DISPLAY", display, 1);
	if (xauthority)
		rc |= setenv("XAUTHORITY", xauthority, 1);
	rc |= setenv("HOME", pw->pw_dir, 1);
	rc |= setenv("SHELL", pw->pw_shell, 1);
	rc |= setenv("USER", pw->pw_name, 1);
	rc |= setenv("LOGNAME", pw->pw_name, 1);
	rc |= setenv("PATH", DEFAULT_PATH, 1);
      out:
	free(term);
	free(display);
	free(xauthority);
	return rc;
}

/**
 * This function will drop the capabilities so that we are left
 * only with access to the audit system. If the user is root, we leave
 * the capabilities alone since they already should have access to the
 * audit netlink socket.
 *
 * Returns zero on success, non-zero otherwise
 */
#if defined(AUDIT_LOG_PRIV) && !defined(NAMESPACE_PRIV)
static int drop_capabilities(int full)
{
	uid_t uid = getuid();
	if (!uid) return 0;

	capng_setpid(getpid());
	capng_clear(CAPNG_SELECT_CAPS);

	if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) < 0) {
		fprintf(stderr, _("Error resetting KEEPCAPS, aborting\n"));
		return -1;
	}

	/* Change uid */
	if (setresuid(uid, uid, uid)) {
		fprintf(stderr, _("Error changing uid, aborting.\n"));
		return -1;
	}

	if (prctl(PR_SET_KEEPCAPS, 0, 0, 0, 0) < 0) {
		fprintf(stderr, _("Error resetting KEEPCAPS, aborting\n"));
		return -1;
	}

	if (! full) 
		capng_update(CAPNG_ADD, CAPNG_EFFECTIVE | CAPNG_PERMITTED, CAP_AUDIT_WRITE);
	return capng_apply(CAPNG_SELECT_CAPS);
}
#elif defined(NAMESPACE_PRIV)
/**
 * This function will drop the capabilities so that we are left
 * only with access to the audit system and the ability to raise
 * CAP_SYS_ADMIN, CAP_DAC_OVERRIDE, CAP_FOWNER and CAP_CHOWN,
 * before invoking pam_namespace.  These capabilities are needed
 * for performing bind mounts/unmounts and to create potential new
 * instance directories with appropriate DAC attributes. If the
 * user is root, we leave the capabilities alone since they already
 * should have access to the audit netlink socket and should have
 * the ability to create/mount/unmount instance directories.
 *
 * Returns zero on success, non-zero otherwise
 */
static int drop_capabilities(int full)
{
	uid_t uid = getuid();
	if (!uid) return 0;

	capng_setpid(getpid());
	capng_clear(CAPNG_SELECT_CAPS);

	if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) < 0) {
		fprintf(stderr, _("Error resetting KEEPCAPS, aborting\n"));
		return -1;
	}

	/* Change uid */
	if (setresuid(uid, uid, uid)) {
		fprintf(stderr, _("Error changing uid, aborting.\n"));
		return -1;
	}

	if (prctl(PR_SET_KEEPCAPS, 0, 0, 0, 0) < 0) {
		fprintf(stderr, _("Error resetting KEEPCAPS, aborting\n"));
		return -1;
	}

	if (! full) 
		capng_updatev(CAPNG_ADD, CAPNG_EFFECTIVE | CAPNG_PERMITTED, CAP_SYS_ADMIN , CAP_FOWNER , CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_AUDIT_WRITE, -1);
	
	return capng_apply(CAPNG_SELECT_CAPS);
}

#else
static inline int drop_capabilities(__attribute__ ((__unused__)) int full)
{
	return 0;
}
#endif

#ifdef NAMESPACE_PRIV
/**
 * This function will set the uid values to be that of caller's uid, and
 * will drop any privilege which may have been raised.
 */
static int transition_to_caller_uid(void)
{
	uid_t uid = getuid();

	if (prctl(PR_SET_KEEPCAPS, 0, 0, 0, 0) < 0) {
		fprintf(stderr, _("Error resetting KEEPCAPS, aborting\n"));
		return -1;
	}

	if (setresuid(uid, uid, uid)) {
		fprintf(stderr, _("Error changing uid, aborting.\n"));
		return -1;
	}
	return 0;
}
#endif

#ifdef AUDIT_LOG_PRIV
/* Send audit message */
static
int send_audit_message(int success, const char *old_context,
		       const char *new_context, const char *ttyn)
{
	char *msg = NULL;
	int rc;
	int audit_fd = audit_open();

	if (audit_fd < 0) {
		fprintf(stderr, _("Error connecting to audit system.\n"));
		return -1;
	}
	if (asprintf(&msg, "newrole: old-context=%s new-context=%s",
		     old_context ? old_context : "?",
		     new_context ? new_context : "?") < 0) {
		fprintf(stderr, _("Error allocating memory.\n"));
		rc = -1;
		goto out;
	}
	rc = audit_log_user_message(audit_fd, AUDIT_USER_ROLE_CHANGE,
				    msg, NULL, NULL, ttyn, success);
	if (rc <= 0) {
		fprintf(stderr, _("Error sending audit message.\n"));
		rc = -1;
		goto out;
	}
	rc = 0;
      out:
	free(msg);
	close(audit_fd);
	return rc;
}
#else
static inline
    int send_audit_message(int success __attribute__ ((unused)),
			   const char *old_context
			   __attribute__ ((unused)),
			   const char *new_context
			   __attribute__ ((unused)), const char *ttyn
			   __attribute__ ((unused)))
{
	return 0;
}
#endif

/**
 * This function attempts to relabel the tty. If this function fails, then
 * the fd is closed, the contexts are free'd and -1 is returned. On success,
 * a valid fd is returned and tty_context and new_tty_context are set.
 *
 * This function will not fail if it can not relabel the tty when selinux is
 * in permissive mode.
 */
static int relabel_tty(const char *ttyn, const char *new_context,
		       char **tty_context,
		       char **new_tty_context)
{
	int fd, rc;
	int enforcing = security_getenforce();
	char *tty_con = NULL;
	char *new_tty_con = NULL;

	if (!ttyn)
		return 0;

	if (enforcing < 0) {
		fprintf(stderr, _("Could not determine enforcing mode.\n"));
		return -1;
	}

	/* Re-open TTY descriptor */
	fd = open(ttyn, O_RDWR | O_NONBLOCK);
	if (fd < 0) {
		fprintf(stderr, _("Error!  Could not open %s.\n"), ttyn);
		return fd;
	}
	/* this craziness is to make sure we can't block on open and deadlock */
	rc = fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
	if (rc) {
		fprintf(stderr, _("Error!  Could not clear O_NONBLOCK on %s\n"), ttyn);
		close(fd);
		return rc;
	}

	if (fgetfilecon(fd, &tty_con) < 0) {
		fprintf(stderr, _("%s!  Could not get current context "
				  "for %s, not relabeling tty.\n"),
			enforcing ? "Error" : "Warning", ttyn);
		if (enforcing)
			goto close_fd;
	}

	if (tty_con &&
	    (security_compute_relabel(new_context, tty_con,
				      string_to_security_class("chr_file"), &new_tty_con) < 0)) {
		fprintf(stderr, _("%s!  Could not get new context for %s, "
				  "not relabeling tty.\n"),
			enforcing ? "Error" : "Warning", ttyn);
		if (enforcing)
			goto close_fd;
	}

	if (new_tty_con)
		if (fsetfilecon(fd, new_tty_con) < 0) {
			fprintf(stderr,
				_("%s!  Could not set new context for %s\n"),
				enforcing ? "Error" : "Warning", ttyn);
			freecon(new_tty_con);
			new_tty_con = NULL;
			if (enforcing)
				goto close_fd;
		}

	*tty_context = tty_con;
	*new_tty_context = new_tty_con;
	return fd;

      close_fd:
	freecon(tty_con);
	close(fd);
	return -1;
}

/**
 * This function attempts to revert the relabeling done to the tty.
 * fd   - referencing the opened ttyn
 * ttyn - name of tty to restore
 * tty_context     - original context of the tty
 * new_tty_context - context tty was relabeled to
 *
 * Returns zero on success, non-zero otherwise
 */
static int restore_tty_label(int fd, const char *ttyn,
			     const char *tty_context,
			     const char *new_tty_context)
{
	int rc = 0;
	char *chk_tty_context = NULL;

	if (!ttyn)
		goto skip_relabel;

	if (!new_tty_context)
		goto skip_relabel;

	/* Verify that the tty still has the context set by newrole. */
	if ((rc = fgetfilecon(fd, &chk_tty_context)) < 0) {
		fprintf(stderr, "Could not fgetfilecon %s.\n", ttyn);
		goto skip_relabel;
	}

	if ((rc = strcmp(chk_tty_context, new_tty_context))) {
		fprintf(stderr, _("%s changed labels.\n"), ttyn);
		goto skip_relabel;
	}

	if ((rc = fsetfilecon(fd, tty_context)) < 0)
		fprintf(stderr,
			_("Warning! Could not restore context for %s\n"), ttyn);
      skip_relabel:
	freecon(chk_tty_context);
	return rc;
}

/**
 * Parses and validates the provided command line options and
 * constructs a new context based on our old context and the
 * arguments specified on the command line. On success
 * new_context will be set to valid values, otherwise its value
 * is left unchanged.
 *
 * Returns zero on success, non-zero otherwise.
 */
static int parse_command_line_arguments(int argc, char **argv, char *ttyn,
					const char *old_context,
					char **new_context,
					int *preserve_environment)
{
	int flag_index;		/* flag index in argv[] */
	int clflag;		/* holds codes for command line flags */
	char *role_s = NULL;	/* role spec'd by user in argv[] */
	char *type_s = NULL;	/* type spec'd by user in argv[] */
	char *type_ptr = NULL;	/* stores malloc'd data from get_default_type */
	char *level_s = NULL;	/* level spec'd by user in argv[] */
	char *range_ptr = NULL;
	const char *new_con = NULL;
	char *tty_con = NULL;
	context_t context = NULL;	/* manipulatable form of new_context */
	const struct option long_options[] = {
		{"role", 1, 0, 'r'},
		{"type", 1, 0, 't'},
		{"level", 1, 0, 'l'},
		{"preserve-environment", 0, 0, 'p'},
		{"version", 0, 0, 'V'},
		{NULL, 0, 0, 0}
	};

	*preserve_environment = 0;
	while (1) {
		clflag = getopt_long(argc, argv, "r:t:l:pV", long_options,
				     &flag_index);
		if (clflag == -1)
			break;

		switch (clflag) {
		case 'V':
			printf("newrole: %s version %s\n", PACKAGE, VERSION);
			exit(0);
		case 'p':
			*preserve_environment = 1;
			break;
		case 'r':
			if (role_s) {
				fprintf(stderr,
					_("Error: multiple roles specified\n"));
				return -1;
			}
			role_s = optarg;
			break;
		case 't':
			if (type_s) {
				fprintf(stderr,
					_("Error: multiple types specified\n"));
				return -1;
			}
			type_s = optarg;
			break;
		case 'l':
			if (!is_selinux_mls_enabled()) {
				fprintf(stderr, _("Sorry, -l may be used with "
						  "SELinux MLS support.\n"));
				return -1;
			}
			if (level_s) {
				fprintf(stderr, _("Error: multiple levels "
						  "specified\n"));
				return -1;
			}
			if (ttyn) {
				if (fgetfilecon(STDIN_FILENO, &tty_con) >= 0) {
					if (selinux_check_securetty_context
					    (tty_con) < 0) {
						fprintf(stderr,
							_
							("Error: you are not allowed to change levels on a non secure terminal \n"));
						freecon(tty_con);
						return -1;
					}
					freecon(tty_con);
				}
			}

			level_s = optarg;
			break;
		default:
			fprintf(stderr, "%s\n", USAGE_STRING);
			return -1;
		}
	}

	/* Verify that the combination of command-line arguments are viable */
	if (!(role_s || type_s || level_s)) {
		fprintf(stderr, "%s\n", USAGE_STRING);
		return -1;
	}

	/* Fill in a default type if one hasn't been specified. */
	if (role_s && !type_s) {
		/* get_default_type() returns malloc'd memory */
		if (get_default_type(role_s, &type_ptr)) {
			fprintf(stderr, _("Couldn't get default type.\n"));
			send_audit_message(0, old_context, new_con, ttyn);
			return -1;
		}
		type_s = type_ptr;
	}

	/* Create a temporary new context structure we extract and modify */
	context = context_new(old_context);
	if (!context) {
		fprintf(stderr, _("failed to get new context.\n"));
		goto err_free;
	}

	/* Modify the temporary new context */
	if (role_s)
		if (context_role_set(context, role_s)) {
			fprintf(stderr, _("failed to set new role %s\n"),
				role_s);
			goto err_free;
		}

	if (type_s)
		if (context_type_set(context, type_s)) {
			fprintf(stderr, _("failed to set new type %s\n"),
				type_s);
			goto err_free;
		}

	if (level_s) {
		range_ptr =
		    build_new_range(level_s, context_range_get(context));
		if (!range_ptr) {
			fprintf(stderr,
				_("failed to build new range with level %s\n"),
				level_s);
			goto err_free;
		}
		if (context_range_set(context, range_ptr)) {
			fprintf(stderr, _("failed to set new range %s\n"),
				range_ptr);
			goto err_free;
		}
	}

	/* Construct the final new context */
	if (!(new_con = context_str(context))) {
		fprintf(stderr, _("failed to convert new context to string\n"));
		goto err_free;
	}

	if (security_check_context(new_con) < 0) {
		fprintf(stderr, _("%s is not a valid context\n"), new_con);
		send_audit_message(0, old_context, new_con, ttyn);
		goto err_free;
	}

	*new_context = strdup(new_con);
	if (!*new_context) {
		fprintf(stderr, _("Unable to allocate memory for new_context"));
		goto err_free;
	}

	free(type_ptr);
	free(range_ptr);
	context_free(context);
	return 0;

      err_free:
	free(type_ptr);
	free(range_ptr);
	/* Don't free new_con, context_free(context) handles this */
	context_free(context);
	return -1;
}

/**
 * Take care of any signal setup
 */
static int set_signal_handles(void)
{
	sigset_t empty;

	/* Empty the signal mask in case someone is blocking a signal */
	if (sigemptyset(&empty)) {
		fprintf(stderr, _("Unable to obtain empty signal set\n"));
		return -1;
	}

	(void)sigprocmask(SIG_SETMASK, &empty, NULL);

	/* Terminate on SIGHUP. */
	if (signal(SIGHUP, SIG_DFL) == SIG_ERR) {
		fprintf(stderr, _("Unable to set SIGHUP handler\n"));
		return -1;
	}

	return 0;
}

/************************************************************************
 *
 * All code used for both PAM and shadow passwd goes in this section.
 *
 ************************************************************************/

int main(int argc, char *argv[])
{
	char *new_context = NULL;	/* target security context */
	char *old_context = NULL;	/* original security context */
	char *tty_context = NULL;	/* current context of tty */
	char *new_tty_context = NULL;	/* new context of tty */

	struct passwd pw;	/* struct derived from passwd file line */
	char *ttyn = NULL;	/* tty path */

	char **old_environ;
	int preserve_environment;

	int fd;
	pid_t childPid = 0;
	char *shell_argv0 = NULL;
	int rc;

#ifdef USE_PAM
	int pam_status;		/* pam return code */
	pam_handle_t *pam_handle;	/* opaque handle used by all PAM functions */

	/* This is a jump table of functions for PAM to use when it wants to *
	 * communicate with the user.  We'll be using misc_conv(), which is  *
	 * provided for us via pam_misc.h.                                   */
	struct pam_conv pam_conversation = {
		misc_conv,
		NULL
	};
#endif

	/*
	 * Step 0: Setup
	 *
	 * Do some initial setup, including dropping capabilities, checking
	 * if it makes sense to continue to run newrole, and setting up
	 * a scrubbed environment.
	 */
	if (drop_capabilities(FALSE)) {
		perror(_("Sorry, newrole failed to drop capabilities\n"));
		return -1;
	}
	if (set_signal_handles())
		return -1;

#ifdef USE_NLS
	setlocale(LC_ALL, "");
	bindtextdomain(PACKAGE, LOCALEDIR);
	textdomain(PACKAGE);
#endif

	old_environ = environ;
	environ = NULL;

	if (!is_selinux_enabled()) {
		fprintf(stderr, _("Sorry, newrole may be used only on "
				  "a SELinux kernel.\n"));
		return -1;
	}

	if (security_getenforce() < 0) {
		fprintf(stderr, _("Could not determine enforcing mode.\n"));
		return -1;
	}

	/*
	 * Step 1: Parse command line and valid arguments
	 *
	 * old_context and ttyn are required for audit logging,
	 * context validation and pam
	 */
	if (getprevcon(&old_context)) {
		fprintf(stderr, _("failed to get old_context.\n"));
		return -1;
	}

	ttyn = ttyname(STDIN_FILENO);
	if (!ttyn || *ttyn == '\0') {
		fprintf(stderr,
			_("Warning!  Could not retrieve tty information.\n"));
	}

	if (parse_command_line_arguments(argc, argv, ttyn, old_context,
					 &new_context, &preserve_environment))
		return -1;

	/*
	 * Step 2:  Authenticate the user.
	 *
	 * Re-authenticate the user running this program.
	 * This is just to help confirm user intent (vs. invocation by
	 * malicious software), not to authorize the operation (which is covered
	 * by policy).  Trusted path mechanism would be preferred.
	 */
	memset(&pw, 0, sizeof(pw));
	if (extract_pw_data(&pw))
		goto err_free;

#ifdef USE_PAM
	if (read_pam_config()) {
		fprintf(stderr,
			_("error on reading PAM service configuration.\n"));
		goto err_free;
	}

	if (app_service_names != NULL && optind < argc) {
		if (strcmp(argv[optind], "-c") == 0 && optind < (argc - 1)) {
			/*
			 * Check for a separate pam service name for the 
			 * command when invoked by newrole.
			 */
			char *cmd = NULL;
			rc = sscanf(argv[optind + 1], "%ms", &cmd);
			if (rc != EOF && cmd) {
				char *app_service_name =
				    (char *)hashtab_search(app_service_names,
							   cmd);
				free(cmd);
				if (app_service_name != NULL)
					service_name = app_service_name;
			}
		}
	}

	pam_status = pam_start(service_name, pw.pw_name, &pam_conversation,
			       &pam_handle);
	if (pam_status != PAM_SUCCESS) {
		fprintf(stderr, _("failed to initialize PAM\n"));
		goto err_free;
	}

	if (!authenticate_via_pam(ttyn, pam_handle))
#else
	if (!authenticate_via_shadow_passwd(pw.pw_name))
#endif
	{
		fprintf(stderr, _("newrole: incorrect password for %s\n"),
			pw.pw_name);
		send_audit_message(0, old_context, new_context, ttyn);
		goto err_close_pam;
	}

	/*
	 * Step 3:  Handle relabeling of the tty.
	 *
	 * Once we authenticate the user, we know that we want to proceed with
	 * the action. Prior to this point, no changes are made the to system.
	 */
	fd = relabel_tty(ttyn, new_context, &tty_context, &new_tty_context);
	if (fd < 0)
		goto err_close_pam;

	/*
	 * Step 4: Fork
	 *
	 * Fork, allowing parent to clean up after shell has executed.
	 * Child: reopen stdin, stdout, stderr and exec shell
	 * Parnet: wait for child to die and restore tty's context
	 */
	childPid = fork();
	if (childPid < 0) {
		/* fork failed, no child to worry about */
		int errsv = errno;
		fprintf(stderr, _("newrole: failure forking: %s"),
			strerror(errsv));
		if (restore_tty_label(fd, ttyn, tty_context, new_tty_context))
			fprintf(stderr, _("Unable to restore tty label...\n"));
		if (close(fd))
			fprintf(stderr, _("Failed to close tty properly\n"));
		goto err_close_pam;
	} else if (childPid) {
		/* PARENT
		 * It doesn't make senes to exit early on errors at this point,
		 * since we are doing cleanup which needs to be done.
		 * We can exit with a bad rc though
		 */
		pid_t pid;
		int exit_code = 0;
		int status;

		do {
			pid = wait(&status);
		} while (pid < 0 && errno == EINTR);

		/* Preserve child exit status, unless there is another error. */
		if (WIFEXITED(status))
			exit_code = WEXITSTATUS(status);

		if (restore_tty_label(fd, ttyn, tty_context, new_tty_context)) {
			fprintf(stderr, _("Unable to restore tty label...\n"));
			exit_code = -1;
		}
		freecon(tty_context);
		freecon(new_tty_context);
		if (close(fd)) {
			fprintf(stderr, _("Failed to close tty properly\n"));
			exit_code = -1;
		}
#ifdef USE_PAM
#ifdef NAMESPACE_PRIV
		pam_status = pam_close_session(pam_handle, 0);
		if (pam_status != PAM_SUCCESS) {
			fprintf(stderr, "pam_close_session failed with %s\n",
				pam_strerror(pam_handle, pam_status));
			exit_code = -1;
		}
#endif
		rc = pam_end(pam_handle, pam_status);
		if (rc != PAM_SUCCESS) {
			fprintf(stderr, "pam_end failed with %s\n",
				pam_strerror(pam_handle, rc));
			exit_code = -1;
		}
		hashtab_map(app_service_names, free_hashtab_entry, NULL);
		hashtab_destroy(app_service_names);
#endif
		free(pw.pw_name);
		free(pw.pw_dir);
		free(pw.pw_shell);
		free(shell_argv0);
		free(new_context);
		return exit_code;
	}

	/* CHILD */
	/* Close the tty and reopen descriptors 0 through 2 */
	if (ttyn) {
		if (close(fd) || close(0) || close(1) || close(2)) {
			fprintf(stderr, _("Could not close descriptors.\n"));
			goto err_close_pam;
		}
		fd = open(ttyn, O_RDWR | O_NONBLOCK);
		if (fd != 0)
			goto err_close_pam;
		rc = fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
		if (rc)
			goto err_close_pam;

		fd = open(ttyn, O_RDWR | O_NONBLOCK);
		if (fd != 1)
			goto err_close_pam;
		rc = fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
		if (rc)
			goto err_close_pam;

		fd = open(ttyn, O_RDWR | O_NONBLOCK);
		if (fd != 2)
			goto err_close_pam;
		rc = fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
		if (rc)
			goto err_close_pam;

	}
	/*
	 * Step 5:  Execute a new shell with the new context in `new_context'. 
	 *
	 * Establish context, namespace and any options for the new shell
	 */
	if (optind < 1)
		optind = 1;

	/* This is ugly, but use newrole's argv for the exec'd shells argv */
	if (asprintf(&shell_argv0, "-%s", pw.pw_shell) < 0) {
		fprintf(stderr, _("Error allocating shell's argv0.\n"));
		shell_argv0 = NULL;
		goto err_close_pam;
	}
	argv[optind - 1] = shell_argv0;

	if (setexeccon(new_context)) {
		fprintf(stderr, _("Could not set exec context to %s.\n"),
			new_context);
		goto err_close_pam;
	}
#ifdef NAMESPACE_PRIV
	/* Ask PAM to setup session for user running this program */
	pam_status = pam_open_session(pam_handle, 0);
	if (pam_status != PAM_SUCCESS) {
		fprintf(stderr, "pam_open_session failed with %s\n",
			pam_strerror(pam_handle, pam_status));
		goto err_close_pam;
	}
#endif

	if (send_audit_message(1, old_context, new_context, ttyn)) {
		fprintf(stderr, _("Failed to send audit message"));
		goto err_close_pam_session;
	}
	freecon(old_context); old_context=NULL;
	freecon(new_context); new_context=NULL;

#ifdef NAMESPACE_PRIV
	if (transition_to_caller_uid()) {
		fprintf(stderr, _("Failed to transition to namespace\n"));
		goto err_close_pam_session;
	}
#endif

	if (drop_capabilities(TRUE)) {
		fprintf(stderr, _("Failed to drop capabilities %m\n"));
		goto err_close_pam_session;
	}
	/* Handle environment changes */
	if (restore_environment(preserve_environment, old_environ, &pw)) {
		fprintf(stderr, _("Unable to restore the environment, "
				  "aborting\n"));
		goto err_close_pam_session;
	}
	execv(pw.pw_shell, argv + optind - 1);

	/*
	 * Error path cleanup
	 *
	 * If we reach here, then we failed to exec the new shell.
	 */
	perror(_("failed to exec shell\n"));
      err_close_pam_session:
#ifdef NAMESPACE_PRIV
	pam_status = pam_close_session(pam_handle, 0);
	if (pam_status != PAM_SUCCESS)
		fprintf(stderr, "pam_close_session failed with %s\n",
			pam_strerror(pam_handle, pam_status));
#endif
      err_close_pam:
#ifdef USE_PAM
	rc = pam_end(pam_handle, pam_status);
	if (rc != PAM_SUCCESS)
		fprintf(stderr, "pam_end failed with %s\n",
			pam_strerror(pam_handle, rc));
#endif
      err_free:
	freecon(tty_context);
	freecon(new_tty_context);
	freecon(old_context);
	freecon(new_context);
	free(pw.pw_name);
	free(pw.pw_dir);
	free(pw.pw_shell);
	free(shell_argv0);
#ifdef USE_PAM
	if (app_service_names) {
		hashtab_map(app_service_names, free_hashtab_entry, NULL);
		hashtab_destroy(app_service_names);
	}
#endif
	return -1;
}				/* main() */
