#!/usr/bin/perl
#
# akcron.pl -- the perl code that actually does the work for akcron.
#              This should be run by the wrapper script akcron ONLY.
#
# Origial by Miles Davis <miles@cs.stanford.edu>
#
# $Id: akcron,v 1.7 2004/06/10 21:08:42 miles Exp $
#

use Getopt::Long;
use Pod::Usage;
use POSIX qw(setuid);
use POSIX qw(setgid);
use POSIX qw(setsid);


##############################################################################
# Preliminaries
##############################################################################
$UID = $<;						# The real UID of this procees.
$EUID = $>;						# The effective UID of this process.
$PID = $$;						# The PID of this process.
$USER  = getpwuid($UID);	# Username of UID
$DEBUG=0;						#assume we don't want to know everything.

# Since these may be tainted, set them explicitly
$ENV{'PATH'} = '/bin:/usr/bin';
#$ENV{'KRB5CCNAME'} = "/tmp/krb5cc_akcron_${UID}_${PID}";
$ENV{'HOSTNAME'}  = `/bin/hostname`;

# Where do we keep the keytabs?
$KEYTAB_DIR = '/afs/cs/etc/akcron';
$KEYTAB = "$KEYTAB_DIR/$USER.keytab";

# Commands we are going to exec via system() or otherwise.
$kinit = '/usr/kerberos/bin/kinit';
$klist = '/usr/kerberos/bin/klist';
$kdestroy = '/usr/kerberos/bin/kdestroy';
$aklog = '/usr/bin/aklog';
$unlog = '/usr/bin/unlog';
$tokens = '/usr/bin/tokens';
$setuid_akcron = '/usr/sbin/setuid-akcron';

##############################################################################
# Setup for command line options.
##############################################################################
my %opts = (
	'verbose+'  => \$DEBUG,
	'help'   => \$help,
	'init'	=>	\$init,
	'execute|command=s'=> \$command,
	'subshell=i'	=>	\$subshell,
	'destroy'=> \$destroy,
);

# Grab the command line options.
GetOptions(%opts) or pod2usage(1);

##############################################################################
# Get to work.
##############################################################################

# This is stupid. Have I forgotten formal logic???
# Only one action can be specified at a time. These options are mututally 
# exclusive.
if (($init and $command) or ($init and $destroy) or ($destroy and $command)) {
	print "Only one action (-i, -c, -d) may be specified at once.\n\n";
	pod2usage(1);
	exit (2);
}

$DEBUG && print "Process ID: $PID\n";
$DEBUG && print "UID: $USER ($UID)\n";
$DEBUG && print "EUID: $EUID\n";

# Ok, nothing silly seems to have been specified on the command line, so see
# what we are supposed to do.
if($help) {
	pod2usage(-exitstatus => 0, -verbose => 2);
} elsif($init) {
	&akcron_init();
} elsif($command) {
	&akcron_exec($command);
} elsif($destroy) {
	&akcron_destroy();
} else {
	# It should never come to this, but just in case...
	pod2usage(1);
	exit 2;
}

#&cleanup();

exit 0;
##############################################################################
# END END END END END END END END END END END END END END END END END END END
##############################################################################


##############################################################################
# Subroutines to do the actual work.
##############################################################################
sub akcron_init() {
	$DEBUG && print "Going to create keytab.\n";
	system("/afs/cs/software/bin/remctl ca1 akcron init");
	return(0);
}

sub akcron_destroy() {
	$DEBUG && print "Going to destroy keytab.\n";
	system("/afs/cs/software/bin/remctl ca1 akcron destroy");
	return(0);
}

sub akcron_exec() {
	my $command = shift;
	$DEBUG && print "Going to execute '$command' using keytab\n";

	if ($command eq '') {
		die("No command specified.");
	}

	#system($klist);
	if ($EUID != 0) {
		#$DEBUG && print "I'm not running as root, but I need to be.\n";
		# Have we already tried to rexec ourselves via the setuid binary?
		# If so, there's a loop, so execution can't proceed.
		if($subshell) {
			print("Loop detected -- the setuid binary failed.\n");
		}
		# Need to become root, so fork:
		#  a) the parent will exit
		#  b) the child should be root and will re-execute akcron setuid.
		$new_pid = fork();
		if($new_pid) {
			# I'm the parent
			wait;
		} else {
			# I'm the child.
			$DEBUG && print "In child process $$. About to exec:\n\t";
			$DEBUG && print "$setuid_akcron '$command'\n";
			exec("$setuid_akcron '$command'") or die "Can't execute $setuid_akcron\n";
			exit;
		}
	} else {
		# We're UID 0. Hooray.

		if($subshell == 0) {
			die("Can't re-exec as root.\n");
		}

		########################################################################
		# Here's what we're going to do:
		#
		# One:   kinit to cron/hostname
		# Two:   aklog
		# Three: change uid back to user
		# Four:  kinit to user/cron
		# Five:  aklog
		# Six:   execute command.
		#
		# We need to be root for step one and two only. Probably only step 1.
		# This is only so we can read the keytab for the system in 
		# /etc/krb5.keytab
		########################################################################

		########################################################################
		# One:   kinit to cron/hostname
		#   krb5 = null
		#   afs  = null
		########################################################################

		# Set this to something different, so it won't interfere with the 
		# user's credentials later on.
		$ENV{'KRB5CCNAME'} = "/tmp/krb5cc_akcron_${UID}_${PID}";
		#$DEBUG && print "KRB5CCNAME is set to $ENV{'KRB5CCNAME'}\n";

		# Convert hostname to lower case, since we're only storing lower case
		# in the kerberos database.
		$hostname = `/bin/hostname`;
		chomp $hostname;
		$hostname =~ tr[A-Z][a-z];

		# Now, kinit to cron/hostname, so we can read the user/cron keytab
		# in step four.
		system("$kinit -c $ENV{'KRB5CCNAME'} -k -t /etc/krb5.keytab cron/$hostname") == 0 
			or die("Could not obtain kerberos ticket for cron/$hostname: $?");

		########################################################################
		# Two:   aklog to get AFS tokens.
		#   krb5 = cron/hostname
		#   afs  = null
		########################################################################
		system("$aklog") == 0 
			or die("Could not obtain afs token for cron.$hostname: $aklog_retval");

		#system($klist);
		#system($tokens);

		# Now that we've got AFS tokens, we can destroy the kerberos credentials 
		# cache.
		system("$kdestroy > /dev/null 2>&1");
		unlink($ENV{'KRB5CCNAME'});

		########################################################################
		# Three:   Drop priviledges back to the original user.
		#   krb5 = null
		#   afs  = cron.hostname
		########################################################################

		$DEBUG && print "I should be uid $subshell\n";
		$UID = $subshell;

		# setuid blows.
		#setuid($UID) == 0 or die("Can't set user id to $UID: $?");
		$> = $UID;
		$< = $UID;

		$USER = getpwuid($UID);
		$DEBUG && print "User is $USER ($UID)\n";

		########################################################################
		# Four:   kinit to user/cron
		#   krb5 = null
		#   afs  = cron.hostname
		########################################################################

		$ENV{'KRB5CCNAME'} = "/tmp/krb5cc_akcron_${UID}_${PID}";
		$DEBUG && print "KRB5CCNAME is set to $ENV{'KRB5CCNAME'}\n";

		system("$kinit -c $ENV{'KRB5CCNAME'} -k -t $KEYTAB_DIR/$USER $USER/cron") == 0 
			or die("Could not obtain kerberos ticket for $USER/cron: $?");

		# We don't need those cron/hostname afs tokens anymore.
		system($unlog);

		########################################################################
		# Five:   aklog
		#   krb5 = user/cron
		#   afs  = null
		########################################################################
		system("$aklog") == 0 
			or die("Could not obtain afs token for $USER.cron: $aklog_retval");

		#system($klist);
		#system($tokens);

		#print "###############################################################\n";
		#print "Trying out a few commands:";
		########################################################################
		# Six:   Execute the command, finally.
		#   krb5 = user/cron
		#   afs  = user.cron
		########################################################################
		$retval = system("$command");
		&cleanup();
		exit($retval);
	}

	return(0);
}

sub cleanup() {
	# If we left any credentials, tokens, or other junk laying around, 
	# clean it up.
	#system($unlog);
	system("$kdestroy > /dev/null 2>&1");

	unlink($ENV{'KRB5CCNAME'});

	$ENV{'KRB5CCNAME'} = '';
}

##############################################################################
# Documentation
##############################################################################

__END__

=head1 NAME

akcron - Execute a command with Kerberos 5 and AFS authentication.

=head1 SYNOPSYS

Usage: akcron [ -v ] [ -i ]
       akcron [ -v ] [ -e|-c <command> ] 
       akcron [ -v ] [ -d ]
       akcron [ --help ]

=head1 OPTIONS

=over 8

=item B<--help>

Print this help message.

=item B<-i>

Create the keytab for <user>/cron, where user is the user id of the process current process.

=item B<-c|-e command>

Execute the command as <user>/cron
   
=item B<-d>

Destroy the keytab for <user>/cron

=item B<-v>

Be verbose, print out information during execution

=back

Only one action (-i, -c|-e, -d) may be specified at a time.

=head1 DESCRIPTION

This program, along with some helper programs can create special <user>/cron principals and AFS users, and execute commands as that user.

In create mode (B<-i>), akcron uses B<remctl> to connect to a server, and runs a helper program to create a Kerberos principal for <user>/cron. The principal's key is then added to a keytab in AFS which is accessible only to AFS users in the group cron:hosts. An AFS user, <user>.cron, is created to match the Kerberos principal. When running create mode, the user must have a valid kerbetos TGT for authentication to remctl. This should mean, in practice, that only a user authenticated to Kerberos as <user> can create a keytab for <user>/cron.

In execute mode (B<-c> or B<-e>), the program re-executes itself via a setuid root helper program in order to read the system keytab, /etc/krb5.keytab. It then obtains a Kerberos ticket for cron/<hostname>, granting it access to the keytab directory in AFS. It then drops root priviledges, returning to the original UID that it was executed as (<user>). Then, Kerberos tickets and AFS tokens are fetched using the keytab for <user>/cron. Finally, the command specified is executed.

In destroy mode (B<-d>), akcron uses B<remctl> once again to connect to a server, and runs a helper program to destroy the keytab in AFS for <user>/cron. Neither the principal nor the AFS user <user>.cron are deleted, to prevent the reuse of AFS UIDs, which could lead to unclean ACL conditions.

=head1 REQUIREMENTS

In order to create keytabs, the program B<remctl> must be present. To execute commands as <user>.cron, the host must have a cron/<hostname> keytab in the system keytab /etc/krb5.keytab.


=head1 SECURITY

Root on an akcron client machine (one which has a cron/hostname principal in /etc/krb5.keytab and a cron.hostname AFS user) can execute commands as any <user>.cron.

=head1 AUTHOR

Miles Davis <miles@cs.stanford.edu>

=head1 LICENSE

Copyright 2004 Board of Trustees, Leland Stanford Jr. University.

Export of this software from the United States of America may require a
specific license from the United States Government.  It is the respon-
sibility of any person or organization contemplating export to obtain
such a license before exporting.

WITHIN THAT CONSTRAINT, permission to use, copy, modify, and distribute
this software and its documentation for any purpose and without fee is
hereby granted, provided that the above copyright notice appear in all
copies and that both that copyright notice and this permission notice
appear in supporting documentation, and that the name of Stanford Uni-
versity not be used in advertising or publicity pertaining to distribu-
tion of the software without specific, written prior permission.  Stan-
ford University makes no representations about the suitability of this
software for any purpose.  It is provided "as is" without express or
implied warranty.

THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


=cut
