/*
 *	aegis - project change supervisor
 *	Copyright (C) 1991-2003 Peter Miller;
 *	All rights reserved.
 *
 *	This program 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.
 *
 *	This program 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 this program; if not, write to the Free Software
 *	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
 *
 * MANIFEST: wrappers around operating system functions
 */

#include <ac/stddef.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <ac/unistd.h>

#include <error.h>
#include <glue.h>
#include <os.h>
#include <sub.h>
#include <trace.h>

static int      become_orig_uid;
static int      become_orig_gid;
static int      become_orig_umask;
static int      become_inited;
static int      become_testing;
static int      become_active;
static int      become_active_uid;
static int      become_active_gid;
static int      become_active_umask;


void
os_setgid(int gid)
{
    os_become_must_not_be_active();
    if (become_testing < 0)
	return;
    if (setgid(gid))
    {
	sub_context_ty  *scp;

	scp = sub_context_new();
	sub_errno_set(scp);
	sub_var_set_long(scp, "Argument", gid);
	if (become_testing)
	{
	    /*
	     * don't use os_testing_mode(),
	     * it could mess with errno
	     */
	    error_intl(scp, i18n("warning: setgid $arg: $errno"));
	}
	else
	    fatal_intl(scp, i18n("setgid $arg: $errno"));
	sub_context_delete(scp);
    }
}


void
os_chown_check(string_ty *path, int mode, int uid, int gid)
{
    struct stat     st;
    int             nerrs;
    int             oret;

    trace(("os_chown_check(path = \"%s\")\n{\n", path->str_text));
    os_become_must_be_active();
    nerrs = 0;
#ifdef S_IFLNK
    oret = glue_lstat(path->str_text, &st);
#else
    oret = glue_stat(path->str_text, &st);
#endif
    if (oret)
    {
	sub_context_ty  *scp;

	scp = sub_context_new();
	sub_errno_set(scp);
	sub_var_set_string(scp, "File_Name", path);
	fatal_intl(scp, i18n("stat $filename: $errno"));
	/* NOTREACHED */
    }
    if (mode > 0 && ((int)st.st_mode & 07777) != mode)
    {
	sub_context_ty *scp;

	scp = sub_context_new();
	sub_var_set_string(scp, "File_Name", path);
	sub_var_set_format(scp, "Number1", "%5.5o", st.st_mode & 07777);
	sub_var_set_format(scp, "Number2", "%5.5o", mode);
	error_intl
	(
	    scp,
	    i18n("$filename: mode is $number1, should be $number2")
	);
	sub_context_delete(scp);
	++nerrs;
    }
    if (become_testing > 0)
    {
	/*
	 * This is the test performed by aegis
	 * in testing (not set-uid-root) mode.
	 */
	if (st.st_uid != geteuid())
	{
	    sub_context_ty *scp;

	    scp = sub_context_new();
	    sub_var_set_string(scp, "File_Name", path);
	    sub_var_set_long(scp, "Number1", st.st_uid);
	    sub_var_set_long(scp, "Number2", geteuid());
	    error_intl
		(scp, i18n("$filename: owner is $number1, should be $number2"));
	    sub_context_delete(scp);
	    ++nerrs;
	}
    }
    else if (become_testing == 0)
    {
	/*
	 * This is the test performed by aegis
	 * when is set-uid-root mode.
	 */
	if ((int)st.st_uid != uid)
	{
	    sub_context_ty *scp;

	    scp = sub_context_new();
	    sub_var_set_string(scp, "File_Name", path);
	    sub_var_set_long(scp, "Number1", st.st_uid);
	    sub_var_set_long(scp, "Number2", uid);
	    error_intl
	    (
		scp,
		i18n("$filename: owner is $number1, should be $number2")
	    );
	    sub_context_delete(scp);
	    ++nerrs;
	}
	if (gid >= 0 && (int)st.st_gid != gid)
	{
	    sub_context_ty *scp;

	    scp = sub_context_new();
	    sub_var_set_string(scp, "File_Name", path);
	    sub_var_set_long(scp, "Number1", st.st_gid);
	    sub_var_set_long(scp, "Number2", gid);
	    error_intl
	    (
		scp,
		i18n("$filename: group is $number1, should be $number2")
	    );
	    sub_context_delete(scp);
	    ++nerrs;
	}
    }
    else
    {
	/*
	 * No test is performed by aereport, aefind, etc.
	 * This is because testing mode and non-testing mode
	 * are indistinguishable.
	 */
    }
    if (nerrs)
    {
	sub_context_ty  *scp;

	scp = sub_context_new();
	sub_var_set_string(scp, "File_Name", path);
	fatal_intl(scp, i18n("$filename: has been tampered with (fatal)"));
	/* NOTREACHED */
    }
    trace(("}\n"));
}


void
os_become_init(void)
{
    assert(!become_inited);
    become_active_umask = DEFAULT_UMASK;
    become_orig_umask = umask(DEFAULT_UMASK);
    become_orig_uid = getuid();
    become_orig_gid = getgid();
#ifdef SINGLE_USER
    become_testing = -1;
    become_active_uid = become_orig_uid;
    become_active_gid = become_orig_gid;
#else
    if (setuid(0))
    {
	become_testing = 1;
	become_active_uid = become_orig_uid;
	become_active_gid = become_orig_gid;
    }
    else
	setgid(0);
#endif
    become_inited = 1;
}


void
os_become_init_mortal(void)
{
    assert(!become_inited);
    become_active_umask = DEFAULT_UMASK;
    become_orig_umask = umask(DEFAULT_UMASK);
    become_orig_uid = getuid();
    become_orig_gid = getgid();
    become_testing = -1;
    become_active_uid = become_orig_uid;
    become_active_gid = become_orig_gid;
    become_inited = 1;
}


int
os_testing_mode(void)
{
    static int      warned;

    assert(become_inited);
    if (become_testing <= 0)
	return 0;
    if (!warned)
    {
	sub_context_ty *scp;

	warned = 1;
	scp = sub_context_new();
	verbose_intl(scp, i18n("warning: test mode"));
	sub_context_delete(scp);
    }
    return 1;
}


void
os_become(int uid, int gid, int um)
{
    trace(("os_become(uid = %d, gid = %d, um = %03o)\n{\n", uid, gid, um));
    if (become_active)
	fatal_raw("multiple user permissions set (bug)");
    become_active = 1;
    if (os_testing_mode())
    {
	become_active_umask = um;
	umask(um);
    }
    else if (become_testing < 0)
    {
	/* do nothing if we are mortal */
	become_active_umask = um;
	umask(um);
    }
    else
    {
	become_active_uid = uid;
	become_active_gid = gid;
	become_active_umask = um;
#ifndef CONF_NO_seteuid
	if (setegid(gid))
	    nfatal("setegid(%d)", gid);
	if (seteuid(uid))
	    nfatal("seteuid(%d)", uid);
	umask(um);
#endif
    }
    trace(("}\n"));
}


void
os_become_undo(void)
{
    trace(("os_become_undo()\n{\n"));
    os_become_must_be_active();
    assert(become_inited);
    become_active = 0;
    if (!become_testing)
    {
#ifndef CONF_NO_seteuid
	if (seteuid(0))
	    nfatal("seteuid(0)");
	if (setegid(0))
	    nfatal("setegid(0)");
#endif
	become_active_uid = 0;
	become_active_gid = 0;
    }
    trace(("}\n"));
}


void
os_become_orig(void)
{
    os_become(become_orig_uid, become_orig_gid, become_orig_umask);
}


void
os_become_query(int *uid, int *gid, int *umsk)
{
    os_become_must_be_active();
    *uid = become_active_uid;
    if (gid)
	*gid = become_active_gid;
    if (umsk)
	*umsk = become_active_umask;
}


void
os_become_orig_query(int *uid, int *gid, int *umsk)
{
    if (uid)
	*uid = become_orig_uid;
    if (gid)
	*gid = become_orig_gid;
    if (umsk)
	*umsk = become_orig_umask;
}


int
os_become_active(void)
{
    return become_active;
}


void
os_become_must_be_active_gizzards(const char *file, int line)
{
    if (!os_become_active())
	fatal_raw("%s: %d: user permissions not set (bug)", file, line);
}


void
os_become_must_not_be_active_gizzards(const char *file, int line)
{
    if (os_become_active())
    {
	fatal_raw
	(
	    "%s: %d: user permissions set when aught not (bug)",
	    file,
	    line
	);
    }
}
