#!/usr/bin/perl -w
########################################################################
# BooNE k5login file administration utility
#
# Chris Green <greenc@fnal.gov>
# UCR / BooNE
# 2002/02/28
#
########################################################################

########################################################################
# Preamble
########################################################################

# Nice Perl
use strict;

# Use the Getopt package
use Getopt::Long;

# Use the File::Basename package
use File::Basename;

# Use the Pod::Usage package
use Pod::Usage;

########################################################################
# Other definitions
########################################################################

# Hash to store options
my %options = ();

# Default items
my $realm = "FNAL.GOV";
my $file = "$ENV{HOME}/.k5login";

# Other useful variables
my $basename = basename($file);
my $exit_code = 0;

########################################################################
# Code
########################################################################

# Proces command-line options
process_options();

my $check_only = ! (exists $options{add}
		   or exists $options{remove}
		   or exists $options{sort});

# Check we have the file and it has the right permissions
verify_k5login(not $check_only);

# Check for principals
$exit_code = check_entries()
  if ((exists $options{check})
      or $check_only);

# Add new principals
$exit_code = add_entries() if exists $options{add};

# Remove principals
$exit_code = remove_entries() if exists $options{remove};

# Sort entries
$exit_code = sort_entries() if exists $options{sort};

# Finish
exit($exit_code);

1;

########################################################################
# Functions
########################################################################

########################################################################
# Function: process_options
#
# Synopsis: Process command-line options.
#
# Arguments: none
#
# Returns: none
#
# Globals: $file
#          $basename
#          %options
#
# Notes: Makes non-returning calls to pod2usage on help or man options,
#        or on illegal options.
#
########################################################################
sub process_options {
  # Options specifiers
  GetOptions(\%options, "check|c:s@", "add|a=s@",
	     "remove|r=s@", "help|h|?", "man|m",
	     "kerberos-realm|k=s" => \$realm,
	     "sort|s", "file|f=s", "user|u=s")
    or pod2usage(-exitval => 2); # short usage (synopsis only)

  # Medium usage (options explanation)
  pod2usage(-verbose => 1) if exists $options{help};

  # Full man page (requires working pod2man)
  pod2usage(-verbose => 2) if exists $options{man};

  # If we've changed the filespec, update the short version
  if (exists $options{file}) {
    $file = $options{file};
    $basename = basename($file);
  }
  if (exists $options{user}) {
    $file = glob "~$options{user}/${basename}";
  }
  return;
}


########################################################################
# Function: verify_k5login
#
# Synopsis: Checks whether $file exists and has correct permissions.
#
# Arguments: $create_if_necessary
#
# Returns: none
#
# Globals: $file
#
# Notes: Makes non-returning calls to local_exit on fatal errors.
#
########################################################################
sub verify_k5login {
  my $create_if_necessary = shift;
  # Check the k5login file exists and has the necessary privileges
  -e "$file"
    or (! $create_if_necessary and return)
    or (system("touch $file") == 0 and
	((exists $options{user})
	 ?(system("chown $options{user} $file") == 0)
	 :1) and
	print "verify: created new file $file\n")
    or local_exit(2, "verify: $file does not exist and was unable to create!");

  my $mode;
  my $dummy;
  ($dummy,$dummy,$mode) = stat "$file";

  # Check and change file permissions
  ($mode % 512) == 0600 or
    ((chmod 0600, "$file") == 1 and
     print "verify: changed permissions of $file to 600\n")
     or local_exit(2, "verify: unable to set permissions of $file to 600");
}


########################################################################
# Function: check_entries
#
# Synopsis: Check for presence or absence of entries in the .k5login
#           file.
#
# Arguments: none
#
# Returns: 0 if all requested entries were found or none specified;
#          1 otherwise.
#
# Globals: $file
#          $basename
#          %options
#
# Notes: If no principals are specified, print all entries and return 0.
#
########################################################################
sub check_entries {
  my $exit_code = 0;
  open K5LOGIN, "$file"
    or local_exit(2, "check: unable to open $file for reading");

  # Check if we have individual principals specified or we want them all
  if ($options{check}->[0] ne "") {
    for (@{$options{check}}) {
      # If return from check_entry is non-zero, set the exit_code
      check_entry($_, 1) and $exit_code = 1;
    }
  } else {
    print "check: listing all $basename entries:\n";
    while (<K5LOGIN>) {
      chomp;
      print"       $_\n";
    }
  }
  close K5LOGIN;
  return $exit_code;
}


########################################################################
# Function: check_entry
#
# Synopsis: Check for an individual entry in the .k5login file
#
# Arguments: $principal
#            $print (optional, defaults to 0)
#
# Returns: 0 if principal found in .k5login, 1 otherwise
#
# Globals: none
#
# Notes: Assumes .k5login file is already open for reading.
#
########################################################################
sub check_entry {
  my $principal = qualify_principal(shift);
  my $print = shift;
  $print = 0 if ! defined $print;

  # Rewind the open file to the beginning
  seek K5LOGIN, 0, 0;

  while (<K5LOGIN>) {
    chomp;
    if ($_ eq $principal) {
      print "check: $principal found\n" if $print;
      return 0;
    }
  }
  print "check: $principal not found\n" if $print;
  return 1;
}


########################################################################
# Function: add_entries
#
# Synopsis: Check for presence of entries and add if absent.
#
# Arguments: none
#
# Returns: 0 if all entries were added, 1 otherwise
#
# Globals: $file
#          $basename
#          %options
#
# Notes: Makes non-returning calls to local_exit on fatal errors.
#
########################################################################
sub add_entries {
  my $exit_code = 0;

  # Open mixed for possible append
  open K5LOGIN, "+<$file"
    or local_exit(2, "add: unable to open $file for read/append");

  foreach my $principal (qualify_principal(@{$options{add}})) {
    # Rewind
    seek K5LOGIN, 0, 0;
    if (check_entry($principal)) {
      # Append
      seek K5LOGIN, 0, 2;
      print K5LOGIN "$principal\n";
      print "add: $principal added to $basename\n";
    } else {
      print "add: $principal already present in $basename\n";
      $exit_code = 1;
    }
  }
  close K5LOGIN;
  return $exit_code;
}


########################################################################
# Function: remove_entries
#
# Synopsis: Remove specified entries from .k5login if present
#
# Arguments: none
#
# Returns: 0
#
# Globals: $file
#          $basename
#          %options
#
# Notes: Makes non-returning calls to local_exit on fatal errors.
#
########################################################################
sub remove_entries {
  my $exit_code = 0;
  open K5LOGIN, "$file";
  my @all_entries = <K5LOGIN>;
  chomp @all_entries;
  my @remove_entries = qualify_principal(@{$options{remove}});
  #######################################################################
  # <hairy-perl>
  #
  # Note the nested grep here: for each principal in @all_entries,
  # quote its metacharacters and then search for it in the
  # @remove_entries list. If it is there, then the false value of (!
  # print) will ensure that the principal is ommitted from the vetted
  # list.
  @all_entries =
    grep {my $quoted_principal=quotemeta $_; my $principal = $_;
	  scalar(grep /^$quoted_principal$/, @remove_entries) == 0 or
	    ! print "remove: removing $principal from $basename\n"}
      @all_entries;
  # </hairy-perl>
  ######################################################################
  close K5LOGIN;
  # Re-write the file with our vetted list
  open K5LOGIN, ">$file"
    or local_exit(2, "remove: unable to open $file for writing");
  print K5LOGIN join("\n", @all_entries), "\n";
  close K5LOGIN;
  return $exit_code
}


########################################################################
# Function: qualify_principal
#
# Synopsis: Check if the principal is properly qualified, and append
#           the default realm if not
#
# Arguments: principal or principals
#
# Returns: scalar or array of corrected principals
#
# Globals: $realm
#
# Notes:
#
########################################################################
sub qualify_principal {
  if ($#_ == 0) { # single argument, return a scalar
    $_ = shift;
    return /\@/?$_:"$_\@$realm"; # return a scalar
  } else { # array, return array
    return map {/\@/?$_:"$_\@$realm"} @{_}; # return a list
  }
}


########################################################################
# Function: sort_entries
#
# Synopsis: Sort and re-write the .k5login file
#
# Arguments: none
#
# Returns: 0
#
# Globals: $file
#
# Notes: Makes non-returning calls to local_exit on fatal errors.
#
########################################################################
sub sort_entries {
  my $exit_code = 0;
  print "sort: sorting $basename ... ";
  open K5LOGIN, "$file";
  my @all_entries = sort grep /\@/, <K5LOGIN>;
  close K5LOGIN;

  # Rewrite sorted entries
  open K5LOGIN, ">$file"
    or local_exit(2, "\nsort: unable to open $file for writing");
  print K5LOGIN @all_entries;
  close K5LOGIN;
  print "done\n";
  return $exit_code;
}


########################################################################
# Function: print_options
#
# Synopsis: Print processed options
#
# Arguments: none
#
# Returns: none
#
# Globals: $options
#
# Notes: Debug only. Not currently used.
#
########################################################################
sub print_options {
  # Diagnostics: print received options
  foreach my $key (keys %options) {
    print "$key=";
  SWITCH: {
      # Check whether the option value is a reference
      my $ref_option = ref $options{$key};
      ! $ref_option && do {
	print $options{$key}, "\n";
	last SWITCH;
      };
      # If it is an array ref, print out the array's values
      "ARRAY" && do {
	print join(", ", @{$options{$key}}), "\n";
	last SWITCH;
      };
      # Don't know how to deal with anything else, but then we shouldn't
      # be getting anything else
      print "<unsupported reference type $ref_option>\n";
    }
  }
}


########################################################################
# Function: local_exit
#
# Synopsis: Exit with specified exit code, printing a message to STDERR
#
# Arguments: $exit_code
#            mesage (optional)
#
# Returns: none
#
# Globals: none
#
# Notes: Does not return.
#
########################################################################
sub local_exit {
  my $code = shift;
  print STDERR @_, "\n" if ($#_>-1);
  exit($code);
}

__END__

########################################################################
# Plain old documentation
########################################################################

=pod

=head1 NAME

k5login-admin - check for, add, remove or sort kerberos principals in
the current user's L<.k5login(5)|.k5login(5)> file.

=head1 SYNOPSIS

B<k5login-admin> B<-h>|B<--help>|B<-?>

B<k5login-admin> B<-m>|B<--man>

[The perl functionality required by the B<-m> option is broken in some
UPS KIT installations of perl.]

B<k5login-admin> [B<-k>|B<--kerberos-realm> I<realm>]
[B<-s>|B<--sort>] [B<-f>|B<--file> I<file>]
[B<-c>|B<--check> [I<principal>]]+
[B<-a>|B<--add> I<principal>]+
[B<-r>|B<--remove> I<principal>]+

Options marked with B<+> are repeatable and cumulative.

=head1 DESCRIPTION

I<k5login-admin> is a tool for easily manipulating L<.k5login(5)|.k5login(5)> files. It
will operate on the F<.k5login> file by default, but alternative
filenames can be specified if desired with the B<-f> switch.

=head1 OPTIONS

=over 4

=item B<-h>

=item B<--help>

=item B<-?>

Short help.


=item B<-m>

=item B<--man>

Full man page information.


=item B<-k> I<realm>

=item B<--kerberos-realm> I<realm>

Append the alternative realm I<realm> to unqualified I<principal>
arguments (B<default> I<FNAL.GOV>).


=item B<-s>

=item B<--sort>

Sort the F<.k5login> file alphabetically.


=item B<-f> I<file>

=item B<--file> I<file>

Use the specified file (B<default> F<~/.k5login>).


=item B<-c> [I<principal>]

=item B<--check> [I<principal>]

If I<principal> is unspecified, print out the full contents of the
F<.k5login> file. Otherwise, check for the presence of the specified
I<principal>. Unqualified principals will be qualified according to
the realm specified by the B<-k> option if present or its default if not.


=item B<-a> I<principal>

=item B<--add> I<principal>

Add the specified I<principal> to the F<.k5login> file if it does not
already exist. Unqualified principals will be qualified according to
the realm specified by the B<-k> option if present or its default if
not.


=item B<-r> I<principal>

=item B<--remove> I<principal>

Remove the specified I<principal> from the F<.k5login> file. Unqualified
principals will be qualified according to the realm specified by the
B<-k> option if present or its default if not.

=back

=head1 RETURN_VALUE

=over 4

=item 0

if all operations successful.

=item 1

if at least one explicit check or add operation failed (remove and
sort operations are always either successful or fatal irrespective of
whether the F<.k5login> file is altered). B<k5login-admin> also exits
with code 1 on help, usage or man page requests.

=item 2

for fatal errors: file opens, invalid command-line options or other
major problems.

=back

=head1 EXAMPLES

=over 4

=item k5login-admin

=item k5login-admin -c

Equivalent to "cat ~/.k5login".

=item k5login-admin -a friend1 -a friend2 -a friend3 -a \ user1@STRANGE.REALM.ORG

Add the principals friend1@FNAL.GOV, friend2@FNAL.GOV and user1@STRANGE.REALM.ORG to the k5login file.

=item k5login-admin -k OTHER.REALM.ORG -a user3 -a user4 -r \ annoyance@FNAL.GOV

Add the principals user3@OTHER.REALM.ORG and user4@OTHER.REALM.ORG and remove the user annoyance@FNAL.GOV.

=back

=head1 FILES

F<.k5login>

=head1 SEE ALSO

kerberos(1), L<.k5login(5)|.k5login(5)>

=head1 AUTHOR

Chris Green <greenc@fnal.gov>.

=cut

