#!/usr/bin/env perl
#
# kcroninit.pl:
#	Script to be used for adding/modifying
#	user/cron/hostname principals.
#
# USAGE:
#	kcroninit
# or
#	kcrondestroy
# (where kcrondestroy is a symlink to the same perl script).
#
# Command Options:
#	-d		debug information
#	-?		get USAGE information
#
#

################################################################################

BEGIN 
{
#    die "KCOMMON_DIR is not defined ... we are quitting.\n"   unless ( $ENV{KCOMMON_DIR} );
    if ( $ENV{KERBEROS_DIR} ) {
	$KERBEROS_DIR = $ENV{KERBEROS_DIR};
    } else {
	$KERBEROS_DIR = "/usr/krb5";
    }
    if ( $ENV{KCOMMON_DIR} ) {
	$KCOMMON_DIR = "$ENV{KCOMMON_DIR}/src";
    } else {
	$KCOMMON_DIR = "$KERBEROS_DIR/lib";
    }
    unshift @INC, "$KCOMMON_DIR";							# kcommon module is here
}
use kcommon;
#  or die "Did you mix UPS and RPM?  $KCOMMON_DIR/kcommon.pl was not found!";
use English;										# english mnemonics
use File::Basename;									# perl module for parsing file names
use File::Copy;										# perl module for copying files
use File::stat;										# perl module for file stats
use Net::Domain qw(hostfqdn);								# to get the fully qualified domain name of the current host
use IPC::Open2;										# allows us to control/monitor both STDIN and STDOUT for calls to kadmin

use Exporter;

@ISA = qw( Exporter );
@EXPORT = qw (
	abort
	);


%OPT = ();										# description of commandline options
$OPT{"d"}  = "debugging mode";
$OPT{"?"}  = "usage information";
$ALL_VALID_OPTS = join("", keys(%OPT));							# all valid options


my $NODENAME = hostfqdn();								# fully qualified domain name for this host.

#
# security checks: before we do anything, make sure that
# the directory wherein we plan to write the keytab file
# is owned by root with the permissions we expect.
# if not, abort with message!
#
my $KEYTAB_DIR = "/var/adm/krb5";							# path to the place where the keytab file is written
my $KEYTAB_DIR_OWNER_UID = 0;								# who should own the directory containing the keytab files
my $KEYTAB_DIR_OWNER_GID = 0;								# group that should own this directory
my $KEYTAB_DIR_PERM = 02711;								# permissions this directory should have

#
# path to kcron command, used to determine keytab file name
#
my $KCRON_COMMAND = "$KERBEROS_DIR/bin/kcron";

#
# path to kcron-create/kcron-destroy commands, used to create the keytab file
#
my $KCRON_CREATE_PATH = "$KERBEROS_DIR/bin";



#
# We set these items (VERB and FLAGS) at the top
# for easier maintenance.  Change it here, where it's easy
# to read, rather than internally to the subroutines
# using the parameters.  These are the flags passed to
# kadmin for each of the various kadmin commands that
# have been implemented.
#

my %VERB;										# verb hash for kadmin things
my %FLAGS;										# corresponding flag hash for kadmin things

$VERB{"check"} = "get_principal";							# when we're just checking the password
$FLAGS{"check"} = "";

$VERB{"add"} = "add_principal";								# when we're adding a new cron principal
$FLAGS{"add"} = "-randkey +requires_preauth";						# flags for adding new cron principal

$VERB{"ktadd"} = "ktadd";								# when we're writing the keytab file
$FLAGS{"ktadd"} = "-k ";								# "-k filename"!!!

$VERB{"destroy"} = "delete_principal";							# when we're deleting a cron principal
$FLAGS{"destroy"} = "-force";								# flags for deleting principals


my $userName = "";									# username running the script
my $userPrincipal = "";									# principal doing kadmin things
my $userPassword = "";									# password for that principal
my $cronPrincipal = "";									# cronprincipal
my $tempKeytabDir = "";									# temporary directory for keytab file
my $tempKeytabFile = "";								# temporary keytab file
my $realKeytabFile = "";								# final keytab file

my %CMDOPTS = ();									# commandline options
my @CMDARGS = ();									# commandline arguments

my $sts;
################################################################################

{
    my @saveARGV = @ARGV;								# save input arguments

    parsecmd( $ALL_VALID_OPTS, 								# parse commandline: $ALL_VALID_OPTS determines what's legal
		\@ARGV,									#  read: input ARGV arguments
		\%CMDOPTS, 								#  returned: hash of commandline options (e.g., opt_a)
		\@CMDARGS )								#  returned: array of commandline arguments
	|| abort("ABORT: failure in parsecmd\n");					# abort if parsing failed.

    if ( $CMDOPTS{"opt_d"} ) {								# are we debugging?
	$DEBUG = $TRUE;									# set flag
    }

    if ( $CMDOPTS{"opt_help"} ) {							# they asked for specific help on that command.
	USAGE();									# call the USAGE sub
	abort("\n");
    }

    confirmKeytabConfiguration() or die("\n");						# make sure the system is configured properly
											#  (this will ABORT if permissions are wrong!)
    confirmSecureChannel() or die("\n");;						# make sure the user is on a secure channel
											#  (this will ABORT if the user says we're not!)

    ($userName, $userPrincipal, $userPassword) = initializeUserParms();			# get user's name, principal and password

    my ($primary, $instance, $realm) = parse_principal( $userPrincipal );		# pull the pieces of the original principal
    $cronPrincipal = "$primary/cron/$NODENAME\@$realm";					# put it together

    $realKeytabFile = "$KEYTAB_DIR/" . constructKeytabFile( $userName );		# get the real location for the final keytab file
    
    if ( $PROGRAM_NAME =~ /kcrondestroy/ ) {
	doCron("destroy", $userPrincipal, $userPassword, $cronPrincipal);		# destroy the cron principal
	doKeytab("destroy", $realKeytabFile);						# destroy the keytab file
    } else {
	doCron("add", $userPrincipal, $userPassword, $cronPrincipal);			# create the cron principal
	$tempKeytabDir  = constructTempDir( $userName );				# get templocation for keytab file
	$tempKeytabFile = "$tempKeytabDir/$userName";					# full filename to temporary keytab file
	print STDOUT ("Now creating empty keytab file for $cronPrincipal...\n");
	$sts = doKcron($realKeytabFile, "kcron-create");				# do the privileged part if we have a tempfile
	if ( !$sts ) {
	    print STDERR ("ERROR creating empty keytab file via 'kcron-create'; ABORT.\n");
	} else {
	    $sts = doKtAdd($userPrincipal, $userPassword, $cronPrincipal, $tempKeytabFile);	# create the temporary keytab file
	    $sts = doKeytab("add", $realKeytabFile, $tempKeytabFile) if ( $sts );		# move it to its final home if we succeeded with the priv'ed part
	    cleanup( $tempKeytabDir );							# remove all vestiges of the temporary copy no matter what
	}
    }
    print STDOUT ("All done.\n");
    exit($SUCCESS);
}
################################################################################
#

sub initializeUserParms {
    my $userName = "";									# username running the script
    my $userPrincipal = "";								# the principal under which kadmin commands should be issued
    my $userPassword  = "";								# the password for this account
    my $currentPrincipal = "";								# current principal
    my $defaultPrincipal = "";								# default: current principal
    my $input = "";									# user input
    my $DONE = $FALSE;									# flag that we have it (and we know it!)
    dbgprint("into initializeUserParms\n");

    # figure out username and current principal (if any).

    my $uid = $REAL_USER_ID;								# get the uid
    $userName = getpwuid($uid);								# convert to username
    my ( $primary, $instance, $realm ) = 						# pull out the pieces we want
	parse_principal($userName);							#  to flesh it out
    $defaultPrincipal = "$primary\@$realm";						# set a baseline default based on username

    if ( open(KLIST, "$KERBEROS_DIR/bin/klist 2>/dev/null |") ) {			# look for current default principal
        while ( <KLIST> ) {
	    chomp;									# strip trailing LF
	    dbgprint("klist line of input: >$_<\n");
	    if ( /[Dd]efault principal: (.*)/ ) {					# if we see the line we want -- grab it!
		$currentPrincipal = $1;							# grab the current principal
		if ( $currentPrincipal ) {
		    dbgprint("currentPrincipal = >$currentPrincipal<\n");
		    my ($primary, $instance, $realm) = 
			parse_principal($currentPrincipal);				# pull the pieces
		    $defaultPrincipal = "$primary\@$realm";				# default is the primary portion of current principal
		}
	    }
	}
        close(KLIST);									# close the klist pipeline
    }

    #
    # we've set the defaults, now confirm the principal and password
    #
    while ( ! $DONE ) {									# loop until we get a valid principal/password combo

	$userPrincipal = readwithDefault("What is your kerberos principal", $defaultPrincipal);
	$userPassword  = readpw("Enter the password for $userPrincipal: ");

	$outcome = kadmin($userPrincipal, $userPassword,				# check that this is the correct password
			\$VERB{"check"}, \$FLAGS{"check"},				#  verb/flags specified above
			$userPrincipal);						#   on this particular principal
	if ( $outcome ) {								# if we got output, we succeeded.
	    $DONE = $TRUE;
	    dbgprint("outcome = >$outcome<\n");
	} else {									# (if it failed, errors went to STDERR and
	    print STDOUT ("Try again.\n");						#  we bombed out completely).
	}
    }

    return($userName, $userPrincipal, $userPassword);					# return the necessary parameters
}
################################################################################
#
sub doCron {
    my ( $what, $userPrincipal, $userPassword, $cronPrincipal ) = @_;
    my $doing = "$what" . "ing";
    my ($cronOutcome);

    dbgprint("into doCron\n");

    print STDOUT ("Now $doing principal $cronPrincipal...\n");

    $cronOutcome = kadmin( $userPrincipal, $userPassword,				# use kadmin command to create the principal
			\$VERB{"$what"}, \$FLAGS{"$what"},
			$cronPrincipal );
    if ( $cronOutcome =~ /Principal \"$cronPrincipal\" created./ ) {			# this message went to STDOUT, user didn't see it
	print STDOUT ("Principal $cronPrincipal created.\n");				# so tell the user.
	return $TRUE;
    } elsif ( $cronOutcome =~ /Principal \"$cronPrincipal\" deleted./ ) {		# this message went to STDOUT, user didn't see it
	print STDOUT ("Principal $cronPrincipal destroyed.\n");				# so tell the user.
	return $TRUE;
    } elsif ( $cronOutcome =~ /already exists/ ) {					# they already saw the message (STDERR)
	return $TRUE;									# not fatal, keep going.
    } else {										# they saw the message, but it sounds bad.
	return $FALSE;									# don't continue.
    }
}
################################################################################
#
sub doKtAdd {
    my ( $userPrincipal, $userPassword, $cronPrincipal, $tempKeytabFile ) = @_;			# create a keytab file via kadmin
    my ($ktaddOutcome);

    $FLAGS{"ktadd"} .= "$tempKeytabFile";							# append the name of the keytab file to flags

    print STDOUT ("Now writing temporary keytab for $cronPrincipal...\n");
    $ktaddOutcome = kadmin( $userPrincipal, $userPassword,
			\$VERB{"ktadd"}, \$FLAGS{"ktadd"},
			$cronPrincipal );
    if ( $ktaddOutcome =~ /Entry for principal $cronPrincipal (.*) added to keytab/ ) {		# if there was an error, the message was
	print STDOUT ("Temporary keytab created.\n");
	return $TRUE;
    } else {
	return $FALSE;
    }
}
################################################################################
#
# run the suid image "kcron-create" to create the 0-block file with the correct ownerships
# and permissions, over which we copy the keytab file; or run "kcron-destroy" to destroy an
# existing file previously created with "kcron-create".
#
sub doKcron {
    my ( $file, $doWhat ) = @_;
    my $status, $notStatus;
    my $rc;
    my @cmdline = ( "$KCRON_CREATE_PATH/$doWhat" );

    dbgprint("now into doKcron to $doWhat $file...");

    $rc = 0xffff & system(@cmdline);
    my $string = sprintf("system(%s) returned %#04x: " , "@cmdline", $rc);
    dbgprint("$string ");
    $status = $FALSE;

    if ( $rc == 0 ) {
	dbgprint("ran with normal exit\n");
	$status = $TRUE;
    } elsif ( $rc == 0xff00 ) {
	dbgprint("command failed: $!\n");
    } elsif ( $rc > 0x80 ) {
	$rc >>= 8;
	dbgprint("ran with non-zero exit status $rc\n");
    } else {
	dbgprint("ran with ");
	if ( $rc & 0x80 ) {
	    $rc &= ~0x80;
	    dbgprint("coredump from ");
	}
	dbgprint("signal $rc\n");
    }
   return ( $status );
}
################################################################################
#
sub doKeytab {
    my ( $what, $realKeytabFile, $tempKeytabFile ) = @_;
    my ($keytabOutcome);
    my $status;

    dbgprint("into doKeytab, realKeytabFile = >$realKeytabFile<, tempKeytabFile = >$tempKeytabFile<\n");
    $status = $TRUE;										# assume success until proven otherwise

    if ( $what eq "destroy" ) {									# if we're destroying,
	print STDOUT ("Now destroying keytab file...\n");
	if ( -f $realKeytabFile ) {								# if the file exists,
	    $keytabOutcome = doKcron($realKeytabFile, "kcron-destroy");				# do the privileged part to remove the file
	    if ( ! $keytabOutcome ) {
		print STDERR ("ERROR destroying keytab file; ABORT.\n");
		$status = $FALSE;
	    } else {
	    print STDOUT ("Keytab file destroyed.\n");
	    }
	} else {
	    print STDOUT ("Keytab file destroyed.\n");						# or maybe it didn't exist, who cares?
	}
    } else {											# if we're creating, it's a bit more complex
	print STDOUT ("Now transferring temporary keytab file contents...\n");
	dbgprint("now doing copy: tempKeytabFile = >$tempKeytabFile<, realKeytabFile = >$realKeytabFile<\n");
	$keytabOutcome = copy($tempKeytabFile, $realKeytabFile);				# copy the temp file into place
	if ( ! $keytabOutcome ) {
	    print STDERR ("ERROR transferring keytab file contents; ABORT.\n");
	    $status = $FALSE;
	} else {
	    print STDOUT ("Transfer complete.\n");
	}
    }
    return $status;
}

################################################################################

sub cleanup {
    my ( $tempDir ) = @_;
    dbgprint("cleaning up, removing >$tempDir<\n");
    system("rm -rf $tempDir >/dev/null 2>&1");
}
################################################################################
#
# the keytab file name is a hash of the username and nodename;
# we just call the kcron program with option "-f" to get the filename.
# It includes a trailing LF.
# NOTE, this feature not available until kerberos v0_5 (and higher).
#
sub constructKeytabFile {
    my ( $userName ) = @_;						# incoming username

    if ( !( -x "$KCRON_COMMAND") ) {					# make sure that the kcron file is executable
	die("This system is not running a recent version of kerberos;\n" .
	    "you must be running kerberos v0_5 or higher to use\n" .
	    "the kcroninit feature.\n");
    } else {
	open( KCRON, "$KCRON_COMMAND -f | " );				# open this channel to get the file name.
	while ( <KCRON> ) {
	    chomp;							# strip trailing linefeed
	    $filename = $_ if ( $_ );					# if it isn't empty, it's the filename.
	}
	close KCRON;							# close the channel
	dbgprint("constructKeytabFile: filename = >$_<\n");
	return $filename;						# return the filename we read
    }
}
################################################################################
#
# Construct a unique name for a writeable temporary directory
# and create the temporary directory such that the user can
# write files into it, and can delete it, but nobody else
# can see it.
# Use a cascading list of possible locations:
#	$TMPDIR
#	/tmp
#	/usr/tmp
#	$HOME
# with some PID+N unique number as the name of the directory.
sub constructTempDir {
    my ($username) = @_;

    dbgprint("into constructTempDir\n");

    my ($pid, $n);							# used for constructing unique directory NAME
    my @potentialParents = (						# the places we look, in this order
	$ENV{"TMPDIR"},
	"/tmp",
	"/usr/tmp",
	$ENV{"HOME"}
    );
    my $tempParent = "";						# parent directory (the one we can write into)
    my $tempDir = "";							# new directory name (the one we create)
    my $possibility;

    foreach $possibility ( @potentialParents ) {			# loop through each potential location
	dbgprint("... $possibility ");
	if ( $possibility ) {						# if it isn't blank (some of the ENV's might be blank)
	    if ( -w $possibility ) {					# can the user write to this area?
		dbgprint("<-YES.\n");
		$tempParent = $possibility;				# aha! use this one as the parent
	    }
	}
	last if ( $tempParent );					# out of the loop if we found one
    }
    if ( ! $tempParent ) {						# couldn't find a writeable temp area?!?!? ABORT!
	die ("Cannot find a writeable temp area -- ABORT!!\n");
    }
    dbgprint("tempParent = >$tempParent<\n");

    $tempDir = $PROCESS_ID;						# start with PID as a reasonable guess of a non-existent directory
    while ( -e "$tempParent/$tempDir" ) {				# until we find a name that does NOT exit, keep looping
	dbgprint("... $tempDir ");
	$tempDir++;							# increment tempdir by 1
    }
    dbgprint("\ntempDir = >$tempDir<\n");
    if ( mkdir( "$tempParent/$tempDir", 0700 ) ) {			# create the temporary directory, if successful return the name
	return "$tempParent/$tempDir";					# return the name of the temporary directory
    }
    die ("Cannot create writeable temp directory $tempParent/$tempDir -- ABORT!!\n");	# ouch!
}

################################################################################
#
# 
# Invoke the kadmin command.  We use open2 so that we can control both
# the input and output of the program (this prevents the adminPassword
# and/or the userPassword from being seen in "ps -ef" output).
# We strip off the "kadmin: " prompt and the "Enter password" prompt, and
# return whatever else was written to the caller.
#
sub kadmin {

    my ( $adminPrincipal, $adminPassword, 					# admin principal and the password
	  $refVerb, $refFlags, 							# which kadmin command and which options
	  $principal, $passwd ) = @_;						# which principal and the password for that principal (if necessary)
    my $cmdPasswordOpt = "";							# if no passwd was passed in, we don't have to pass it to kadmin
    $cmdPasswordOpt = "-pw $passwd" if ( $passwd );				# but (for adding principals, etc.) if there's a password, we need to pass it in

    my $outcome = "";								# keep track of what kadmin said

    my $COMMAND = "$KERBEROS_DIR/sbin/kadmin -p $adminPrincipal";
    dbgprint("ready to invoke >$COMMAND<\n");
    my $pid = open2(*KADMIN_OUT, *KADMIN_IN, $COMMAND);
    print KADMIN_IN "$adminPassword\n";						# first we need to enter the password
    print KADMIN_IN "$$refVerb $$refFlags $cmdPasswordOpt $principal\n";	# now we send the kadmin command we want to invoke
    print KADMIN_IN "exit";
    close(KADMIN_IN);

    while ( $record = <KADMIN_OUT> ) {						# read what it sends back to us
	dbgprint("read a record = >$record<\n");
	$outcome .= $record;							# append to the outcome
    }
    close(KADMIN_OUT);								# close the output channel
#
#   In this method of sending the command, we *always*
#   get a "Enter password:\n" prompt.  So strip this
#   part (which we don't care about).  We also get the "kadmin: "
#   prompt, which we don't care about.  So strip this too.
#
    dbgprint("Before stripping password query: >$outcome<\n");
    $outcome =~ s/Enter password:(\s*)//gi;
    $outcome =~ s/kadmin:(\s*)//gi;
    chomp($outcome);

    dbgprint("\nAfter stripping password query:  >$outcome<\n");
    return $outcome;								# return whatever kadmin said, for subsequent parsing
}
################################################################################
#
# make sure that the directory where we plan to put the keytab files
# has the correct ownership and permissions.  If not, ABORT with
# an error message.  Somebody has mucked with the system!
#

sub confirmKeytabConfiguration {
    my $st;								# return from stat on keytab file directory
    my $ownerOK = $FALSE;						# assume BAD status until proven otherwise
    my $groupOK = $FALSE;						# assume BAD status until proven otherwise
    my $permsOK = $FALSE;						# assume BAD status until proven otherwise
    dbgprint("confirmKeytabConfiguration:\n");

    $st = stat( $KEYTAB_DIR );						# stat the file
    if ( ! defined($st) ) {						# if not defined, then the file doesn't exist.
	dbgprint("$KEYTAB_DIR does not exist yet.\n");
	return $TRUE;							# we'll just let the "kcron-create" make the file.
    }

    # we know the file exists, let's check ownership and permissions
    dbgprint(sprintf("$KEYTAB_DIR exists, UID = >%d<,  GID = >%d<,  permission = >%o<\n", $st->uid, $st->gid, $st->mode));

    $ownerOK = ($st->uid == $KEYTAB_DIR_OWNER_UID);			# ok if the uid matches
    $groupOK = ($st->gid == $KEYTAB_DIR_OWNER_GID);			# ok if the gid matches
    $permsOK = (($KEYTAB_DIR_PERM & $st->mode) == $KEYTAB_DIR_PERM);	# ok if bitwise AND matches

    dbgprint("ownerOK: >$ownerOK<    groupOK: >$groupOK<     permsOK: >$permsOK<\n");

    if ( $ownerOK && $groupOK && $permsOK ) {				# if everything is ok, we're fine.
	return $TRUE;							# get back to the mainland.
    } else {
	print STDERR <<EOFKeytabAbort;

 *************************************************************************
 *                                                                       *
 *      This system is not properly configured to initialize             *
 *      authenticating cron jobs in a secure fashion.                    *
 *                                                                       *
 *      Please contact your sysadmin regarding the ownership and/or      *
 *      permissions on the $KEYTAB_DIR directory.                      *
 *                                                                       *
 *************************************************************************
EOFKeytabAbort
	return($FALSE);							# BAD STATUS!
    }
}

################################################################################

sub confirmSecureChannel {
    my $continue = "";
    my $okSecure = $FALSE;

    print STDOUT <<EOSecureWarning;

 *************************************************************************
 *                                                                       *
 *   NOTE: You will be required to enter your kerberos password.         *
 *                                                                       *
 *   YOU MUST BE ON A SECURE CHANNEL (e.g., you must be running          *
 *   this script on your local machine, or you must be connected         *
 *   via an encrypted session).                                          *
 *                                                                       *
 *   IF YOU ARE NOT ON A SECURE CHANNEL, DO NOT CONTINUE!                *
 *                                                                       *
 *************************************************************************

EOSecureWarning

    $continue = readwithDefault("Are you on a secure channel? ", "y");
    if ( $continue =~ /^[Yy]/ ) {
	$okSecure = $TRUE;
    } else {
	print STDERR ("ABORT, insecure channel.\n");
	$okSecure = $FALSE;
    }
    return $okSecure;
}
################################################################################

sub USAGE {
    my ($PROG) = fileparse($PROGRAM_NAME);
    my ($thing, $dsc);

    print STDOUT <<EOUSAGE;
Usage:

    \$ $PROG [options]

Options:

EOUSAGE
    foreach $thing ( sort (keys %OPT) ) {
	$dsc = $OPT{"$thing"};
	if ( $thing =~ /:$/ ) {							# if options has :, it takes a value
	    print STDOUT ("\t-$thing value\t$dsc\n");
	} else {
	    print STDOUT ("\t-$thing\t\t$dsc\n");
	}
    }
    print STDOUT  ("\nFor more information, see the man page for $PROG.\n");
    return $SUCCESS;
}
################################################################################

