/* 
 * $Id: su.c,v 1.23 1999/02/18 12:29:50 saw Rel $
 *
 * ( based on an implementation of `su' by
 *
 *     Peter Orbaek  <poe@daimi.aau.dk>
 *
 * obtained from ftp://ftp.daimi.aau.dk/pub/linux/poe/ )
 *
 * Rewritten for Linux-PAM by Andrew Morgan <morgan@linux.kernel.org>
 *
 * Modified by Andrey V. Savochkin <saw@msu.ru>
 *
 */

#define ROOT_UID                  0
#define DEFAULT_HOME              "/"
#define DEFAULT_SHELL             "/bin/sh"
#define SLEEP_TO_KILL_CHILDREN    3  /* seconds to wait after SIGTERM before
					SIGKILL */
#define SU_FAIL_DELAY     2000000    /* usec on authentication failure */

#include <stdlib.h>
#include <signal.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <string.h>
#include <syslog.h>
#include <errno.h>

#include <security/pam_appl.h>
#include <security/pam_misc.h>

#include "../../common/include/config.h"

#ifdef HAVE_PWDB
#include <pwdb/pwdb_public.h>
#endif

#include "../../common/include/shell_args.h"
#include "../../common/include/wait4shell.h"
#include "../../common/include/su_indep.h"
#include "../../common/include/checkfds.h"
#include "../include/make_env.h"
#include "../include/setcred.h"
#include "../include/wtmp-gate.h"

/* -------------------------------------------- */
/* ------ declarations ------------------------ */
/* -------------------------------------------- */

static pam_handle_t *pamh = NULL;
static int state;

#define SU_STATE_PAM_INITIALIZED     1
#define SU_STATE_AUTHENTICATED       2
#define SU_STATE_AUTHORIZED          3
#define SU_STATE_SESSION_OPENED      4
#define SU_STATE_CREDENTIALS_GOTTEN  5
#define SU_STATE_PROCESS_UNKILLABLE  6
#define SU_STATE_TERMINAL_REOWNED    7
#define SU_STATE_UTMP_WRITTEN        8

static void exit_now(int exit_code, const char *format, ...);
static void exit_child_now(int exit_code, const char *format, ...);
static void do_pam_init(const char *user, int is_login);
static void su_exec_shell(const char *shell, uid_t uid, int is_login
			  , const char *command, const char *user);

/* -------------------------------------------- */
/* ------ the application itself -------------- */
/* -------------------------------------------- */

void main(int argc, char *argv[])
{
    int retcode, is_login, status;
    int retval, final_retval; /* PAM_xxx return values */
    const char *command, *user;
    const char *shell;
    pid_t child;
    uid_t uid;
    const char *place, *err_descr;

    checkfds();

    /*
     * Check whether stdin is a terminal and store terminal modes for later.
     */
    store_terminal_modes();

    /*
     * Turn off terminal signals - this is to be sure that su gets a
     * chance to call pam_end() in spite of the frustrated user
     * pressing Ctrl-C. (Only the superuser is exempt in the case that
     * they are trying to run su without a controling tty).
     */
    disable_terminal_signals();

    /* ------------ parse the argument list ----------- */

    parse_command_line(argc, argv, &is_login, &user, &command);

    /* ------ initialize the Linux-PAM interface ------ */

#ifdef HAVE_PWDB
    retcode = pwdb_start();
    if (retcode != PWDB_SUCCESS)
	exit_now(1, "su: failed\n");
#endif

    do_pam_init(user, is_login);      /* call pam_start and set PAM items */
    user = NULL;                      /* get this info later (it may change) */

    /*
     * Note. We have forgotten everything about the user. We will get
     * this info back after the user has been authenticated..
     */

    /*
     * Starting from here all changes to the process and environment
     * state are reflected in the change of "state".
     * Random exits are strictly prohibited :-)  (SAW)
     */
    status = 1;                       /* fake exit status of a child */
    err_descr = NULL;                 /* errors hasn't happened */
    state = SU_STATE_PAM_INITIALIZED; /* state -- initial */

    do {                              /* abuse loop to avoid using goto... */

	place = "pam_authenticate";
        retval = pam_authenticate(pamh, 0);	   /* authenticate the user */
	if (retval != PAM_SUCCESS)
            break;
	state = SU_STATE_AUTHENTICATED;

	/*
	 * The user is valid, but should they have access at this
	 * time?
	 */
	place = "pam_acct_mgmt";
        retval = pam_acct_mgmt(pamh, 0);
	if (retval != PAM_SUCCESS) {
	    if (getuid() == 0) {
		(void) fprintf(stderr, "Account management:- %s\n(Ignored)\n"
                               , pam_strerror(pamh, retval));
	    } else
		break;
	}
	state = SU_STATE_AUTHORIZED;

	/* Open the su-session */
	place = "pam_open_session";
        retval = pam_open_session(pamh, 0);     /* Must take care to close */
	if (retval != PAM_SUCCESS)
            break;
	/*
         * Do not advance the state to SU_STATE_SESSION_OPENED here.
         * The session will be closed explicitly if the next step fails.
         */

	/*
	 * Obtain all of the new credentials of the user
	 */
	place = "set_user_credentials";
        retval = set_user_credentials(pamh, is_login, &user, &uid, &shell);
	if (retval != PAM_SUCCESS) {
	    (void) pam_close_session(pamh,retval);
	    break;
	}
	state = SU_STATE_CREDENTIALS_GOTTEN;
	
	/*
         * Prepare the new session: ...
         */
        if (make_process_unkillable(&place, &err_descr) != 0)
	    break;
	state = SU_STATE_PROCESS_UNKILLABLE;

        /*
         * ... setup terminal, ...
         */
        retcode = change_terminal_owner(uid, is_login
                , &place, &err_descr);
	if (retcode > 0) {
	    (void) fprintf(stderr, "su: %s: %s\n", place, err_descr);
	    err_descr = NULL; /* forget about the problem */
	} else if (retcode < 0)
	    break;
	state = SU_STATE_TERMINAL_REOWNED;

        /*
         * ... make [uw]tmp entries.
         */
        if (is_login) {
            /*
             * Note: we use the parent pid as a session identifier for
             * the logging.
             */
            retcode = utmp_open_session(pamh, getpid(), &place, &err_descr);
            if (retcode > 0) {
                (void) fprintf(stderr, "su: %s: %s\n", place, err_descr);
                err_descr = NULL; /* forget about the problem */
            } else if (retcode < 0)
                break;
            state = SU_STATE_UTMP_WRITTEN;
        }

	/* this is where we execute the user's shell */
        child = fork();
        if (child == -1) {
            place = "fork";
            err_descr = strerror(errno);
            break;
        }

        if (child == 0) {       /* child exec's shell */
            su_exec_shell(shell, uid, is_login, command, user);
            /* never reached */
        }

	/* wait for child to terminate */

        /* job control is off for login sessions */
        prepare_for_job_control(!is_login && command != NULL);
	status = wait_for_child(child);
	if (status != 0)
	    D(("shell returned %d", status));

    }while (0);                       /* abuse loop to avoid using goto... */

    if (retval != PAM_SUCCESS) {      /* PAM has failed */
	(void) fprintf(stderr, "su: %s\n", pam_strerror(pamh, retval));
	final_retval = PAM_ABORT;
    } else if (err_descr != NULL) {   /* a system error has happened */
	(void) fprintf(stderr, "su: %s: %s\n", place, err_descr);
	final_retval = PAM_ABORT;
    } else
	final_retval = PAM_SUCCESS;

    /* do [uw]tmp cleanup */
    if (state >= SU_STATE_UTMP_WRITTEN) {
        retcode = utmp_close_session(pamh, &place, &err_descr);
        if (retcode)
            (void) fprintf(stderr, "su: %s: %s\n", place, err_descr);
    }

    /* return terminal to local control */
    if (state >= SU_STATE_TERMINAL_REOWNED)
	restore_terminal_owner();

    /*
     * My impression is that PAM expects real uid to be restored.
     * Effective uid of the process is kept
     * unchanged: superuser.  (SAW)
     */
    if (state >= SU_STATE_PROCESS_UNKILLABLE)
	make_process_killable();

    if (state >= SU_STATE_CREDENTIALS_GOTTEN) {
	D(("setcred"));
	/* Delete the user's credentials. */
	retval = pam_setcred(pamh, PAM_DELETE_CRED);
	if (retval != PAM_SUCCESS) {
	    (void) fprintf(stderr, "WARNING: could not delete credentials\n\t%s\n"
		    , pam_strerror(pamh,retval));
	}
    }

    if (state >= SU_STATE_SESSION_OPENED) {
	D(("session %p", pamh));

	/* close down */
	retval = pam_close_session(pamh,0);
	if (retval != PAM_SUCCESS)
	    (void) fprintf(stderr, "WARNING: could not close session\n\t%s\n"
                           , pam_strerror(pamh,retval));
    }

    /* clean up */
    D(("all done"));
    (void) pam_end(pamh, final_retval);
    pamh = NULL;

#ifdef HAVE_PWDB
    while ( pwdb_end() == PWDB_SUCCESS );
#endif

    /* reset the terminal */
    if (reset_terminal_modes() != 0 && !status)
	status = 1;

    exit(status);                 /* transparent exit */
}

/* -------------------------------------------- */
/* ------ some local (static) functions ------- */
/* -------------------------------------------- */

/* ------ abnormal termination ---------------- */

static void exit_now(int exit_code, const char *format, ...)
{
    va_list args;

    va_start(args,format);
    vfprintf(stderr, format, args);
    va_end(args);

    if (pamh != NULL)
	pam_end(pamh, exit_code ? PAM_ABORT:PAM_SUCCESS);

#ifdef HAVE_PWDB
    while (pwdb_end() == PWDB_SUCCESS);                       /* clean up */
#endif /* HAVE_PWDB */

    /* USER's shell may have completely broken terminal settings
       restore the sane(?) initial conditions */
    reset_terminal_modes();

    exit(exit_code);
}

static void exit_child_now(int exit_code, const char *format, ...)
{
    va_list args;

    va_start(args,format);
    vfprintf(stderr, format, args);
    va_end(args);

    if (pamh != NULL)
	pam_end(pamh, (exit_code ? PAM_ABORT:PAM_SUCCESS)
#ifdef PAM_DATA_QUIET
		     | PAM_DATA_QUIET
#endif
	);

#ifdef HAVE_PWDB
    while (pwdb_end() == PWDB_SUCCESS);                       /* clean up */
#endif /* HAVE_PWDB */

    exit(exit_code);
}

/* ------ PAM setup --------------------------- */

static struct pam_conv conv = {
    misc_conv,                   /* defined in <pam_misc/libmisc.h> */
    NULL
};

static void do_pam_init(const char *user, int is_login)
{
    int retval;

    retval = pam_start("su", user, &conv, &pamh);
    if (retval != PAM_SUCCESS) {
	/*
	 * From my point of view failing of pam_start() means that
	 * pamh isn't a valid handler. Without a handler
	 * we couldn't call pam_strerror :-(   1998/03/29 (SAW)
	 */
	(void) fprintf(stderr, "su: pam_start failed with code %d\n", retval);
	exit(1);
    }

    /*
     * Fill in some blanks
     */

    retval = make_environment(pamh, !is_login);
    D(("made_environment returned: %s", pam_strerror(pamh,retval)));

    if (retval == PAM_SUCCESS && is_terminal) {
	const char *terminal = ttyname(STDIN_FILENO);
	if (terminal) {
	    retval = pam_set_item(pamh, PAM_TTY, (const void *)terminal);
	} else {
	    retval = PAM_PERM_DENIED;                /* how did we get here? */
	}
	terminal = NULL;
    }

    if (retval == PAM_SUCCESS && is_terminal) {
	const char *ruser = getlogin();      /* Who is running this program? */
	if (ruser) {
	    retval = pam_set_item(pamh, PAM_RUSER, (const void *)ruser);
	} else {
	    retval = PAM_PERM_DENIED;             /* must be known to system */
	}
	ruser = NULL;
    }

    if (retval == PAM_SUCCESS) {
	retval = pam_set_item(pamh, PAM_RHOST, (const void *)"localhost");
    }

    if (retval != PAM_SUCCESS) {
	exit_now(1, "su: problem establishing environment\n");
    }

#ifdef HAVE_PAM_FAIL_DELAY
    /* have to pause on failure. At least this long (doubles..) */
    retval = pam_fail_delay(pamh, SU_FAIL_DELAY);
    if (retval != PAM_SUCCESS) {
	exit_now(1, "su: problem initializing failure delay\n");
    }
#endif /* HAVE_PAM_FAIL_DELAY */
}

/* ------ shell invoker ----------------------- */

static void su_exec_shell(const char *shell, uid_t uid, int is_login
			  , const char *command, const char *user)
{
    char * const * shell_args;
    char * const * shell_env;
    const char *pw_dir;
    int retval;

    /*
     * Now, find the home directory for the user
     */

    pw_dir = pam_getenv(pamh, "HOME");
    if ( !pw_dir || pw_dir[0] == '\0' ) {
	/* Not set so far, so we get it now. */
	struct passwd *pwd;

	pwd = getpwnam(user);
	if (pwd != NULL && pwd->pw_name != NULL) {
	    pw_dir = x_strdup(pwd->pw_name);
	}

	/* Last resort, take default directory.. */
	if ( !pw_dir || pw_dir[0] == '\0') {
	    (void) fprintf(stderr, "setting home directory for %s to %s\n"
                           , user, DEFAULT_HOME);
	    pw_dir = DEFAULT_HOME;
	}
    }

    /*
     * We may wish to change the current directory.
     */

    if (is_login && chdir(pw_dir)) {
	exit_child_now(1, "%s not available; exiting\n", pw_dir);
    }

    /*
     * If it is a login session, we should set the environment
     * accordingly.
     */

    if (is_login
	&& pam_misc_setenv(pamh, "HOME", pw_dir, 0) != PAM_SUCCESS) {
	D(("failed to set $HOME"));
	(void) fprintf(stderr
                       , "Warning: unable to set HOME environment variable\n");
    }

    /*
     * Break up the shell command into a command and arguments
     */

    shell_args = build_shell_args(shell, is_login, command);
    if (shell_args == NULL) {
	exit_child_now(1, "su: could not identify appropriate shell\n");
    }

    /*
     * and now copy the environment for non-PAM use
     */

    shell_env = pam_getenvlist(pamh);
    if (shell_env == NULL) {
	exit_child_now(1, "su: corrupt environment\n");
    }

    /*
     * close PAM (quietly = this is a forked process so ticket files
     * should *not* be deleted logs should not be written - the parent
     * will take care of this)
     */

    D(("pam_end"));
    retval = pam_end(pamh, PAM_SUCCESS
#ifdef PAM_DATA_QUIET
		     | PAM_DATA_QUIET
#endif
	);
    pamh = NULL;
    user = NULL;                            /* user's name not valid now */
    if (retval != PAM_SUCCESS) {
	exit_child_now(1, "su: failed to release authenticator\n");
    }

#ifdef HAVE_PWDB
    while ( pwdb_end() == PWDB_SUCCESS );            /* forget all */
#endif

    /* assume user's identity */
    if (setuid(uid) != 0) {
	exit_child_now(1, "su: cannot assume uid\n");
    }

    /*
     * Restore a signal status: information if the signal is ingored
     * is inherited accross exec() call.  (SAW)
     */
    enable_terminal_signals();

    execve(shell_args[0], shell_args+1, shell_env);
    exit_child_now(1, "su: exec failed\n");
}
