#! /usr/bin/perl
                            
# 
# Copyright 1999-2006 University of Chicago
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
# http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# 

# grid-mapfile-check-consistency

use strict;
use warnings;

use English;
use FileHandle;
use File::Spec;
use Getopt::Long;

# Prototypes
sub main ( @ );
sub check_gridmap_location ();
sub parse_options ();
sub display_help ();
sub display_version ();
sub verify_mapfile_existence ();
sub load_gridmap_file ( $ );
sub check_valid_login_names ( $ );

# Gridmap file name, handle, and hash
my $GRID_MAP_FILE = "";
my $gridmap_handle;
my %LINES;

# Gridmap file check status codes
my $existence_check = 0;
my $load_check = 0;
my $valid_login_names_check = 0;

# DiRT support
# DIRT_TIMESTAMP="1436979015"
# DIRT_BRANCH_ID="85"

# Invoke "main"
main ( @ARGV );

#######################################################################
# Function: main
# Description:
#       Main function of the program
# Parameters:
#       command line options
# Returns:
#       0 if checks passed
#       1 if gridmap file is not readable
#       2 if gridmap file is empty
#       3 if loading gridmap file failed (e.g. hardware errors)
#       4 if there are invalid login names in gridmap file
#######################################################################
sub main ( @ ) 
{
	check_gridmap_location();

	parse_options();

	print "Checking " . $GRID_MAP_FILE . " grid mapfile\n";

	print "Verifying grid mapfile existence...";
	$existence_check = verify_mapfile_existence();
	if ($existence_check == 1) 
	{
		print "ERROR: " . $GRID_MAP_FILE . " file is not readable\n";
		exit 1;
	} 
	elsif ($existence_check == 2)
	{
	    print "\nGrid mapfile " . $GRID_MAP_FILE . " is empty\n";
	    exit 2;	
	}
	
	print "OK\n";

	print "Checking for duplicate entries...";
	$load_check = load_gridmap_file(\%LINES);
	if ($load_check == 1) 
	{	
		print "ERROR: Cannot load " . $GRID_MAP_FILE . "\n";
		exit 3;
	} 
	elsif ($load_check == 0) 
	{
		print "OK\n";
	}

	print "Checking for valid user names...";
	$valid_login_names_check = check_valid_login_names(\%LINES);
	if ($valid_login_names_check == 0) 
	{
		print "OK\n";
	} 
	elsif ($valid_login_names_check == 1) 
	{
		exit 4;
	}
	exit 0;
}


#######################################################################
# Helper functions
#######################################################################
# Function: check_gridmap_location
# Description:
#       Checks location of gridmap file conforming to rules 
#       defined by GSI
# Parameters:
#       None
# Returns:
#       None
#######################################################################
sub check_gridmap_location ()
{
	if (defined $ENV{"GRIDMAP"}) 
	{
		$GRID_MAP_FILE = $ENV{"GRIDMAP"};
	}
	else
	{
		if ($REAL_USER_ID == 0) 
		{
			$GRID_MAP_FILE = "/etc/grid-security/grid-mapfile";
		} 
		else 
		{
			my $local_gridmap_file = $ENV{"HOME"} . "/.gridmap";	
			if (-e $local_gridmap_file && -r $local_gridmap_file) 
			{				
				$GRID_MAP_FILE = $local_gridmap_file;
			}
			else 
			{
				$GRID_MAP_FILE = "/etc/grid-security/grid-mapfile";
			}
		}
	}	
}

#######################################################################
# Function: parse_options
# Description:
#       Parses script options. If gridmap file was specified, 
#       new value overrides previous one.
# Parameters:
#       None
# Returns:
#       0 help of version options selected
#######################################################################
sub parse_options ()
{
	my $parser;
	
	my $help = 0;
	my $version = 0;
	my $mapfile = "";
	
	$parser = new Getopt::Long::Parser;
                      
	$parser->getoptions("usage|help|h" => \$help, 
					    "version|v" => \$version,
					    "mapfile|file|f=s" => \$mapfile);

	if ($help == 1) 
	{	
		display_help();
		exit 0;
	} 
	elsif ($version == 1) 
	{
		display_version();
		exit 0;
	} 
	elsif ($mapfile ne "") 
	{
	    $GRID_MAP_FILE = File::Spec->rel2abs($mapfile);	
	}	
}

#######################################################################
# Function: display_help
# Description:
#       Displays help information
# Parameters:
#       None
# Returns:
#       None
#######################################################################
sub display_help ()
{
    my ($volume, $directory, $file) = File::Spec->splitpath($0);	
	
	print $file . " checks the consistency of the Grid mapfile\n";
	print "Options:\n";
	print "--help, -help, --usage, -usage, --h, -h             Displays help\n";
	print "--version, -version, --v, -v                        Displays version\n";
	print "--mapfile FILE, -mapfile FILE, --file FILE,
-file FILE, --f FILE, -f FILE                       Path of gridmap to be used\n";
}

#######################################################################
# Function: display_version
# Description:
#       Displays version information
# Parameters:
#       None
# Returns:
#       None
#######################################################################
sub display_version () 
{    
    my ($volume, $directory, $file) = File::Spec->splitpath($0);
   
    my $program_version = '$Revision$';    
    $program_version =~ /Revision: ([\d\.]+)/;
    print $file . ": " . $1 . "\n";
}

######################################################################
# Function: verify_mapfile_existence
# Description:
#       If a gridmap file is a symlink, follows it.
#       Checks if the gridmap file is readable.
#       Checks if the gridmap file is writable.
#       Checks if the gridmap file is empty.
#       If the file is either non-writable or empty,
#       appropriate warning is printed.
# Parameters:
#       None
# Returns:
#       0 gridmap file is readable, writable and non-empty
#       1 gridmap file is non-readable (failed)
#       2 gridmap file is non-writable
#######################################################################
sub verify_mapfile_existence ()
{	
	if (-l $GRID_MAP_FILE) 
	{
		my $link = readlink($GRID_MAP_FILE);
		$GRID_MAP_FILE = $link;
	}
	
	if (! -r $GRID_MAP_FILE) 
	{
		return 1;
	}
	
	if (! -w $GRID_MAP_FILE) 
	{		
		print "\nWARNING: Grid mapfile " . $GRID_MAP_FILE . " is not writable\n";
	}
	
	if (-z $GRID_MAP_FILE) 
	{		
		return 2;
	}
	
	return 0;
}


#####################################################
# Function: load_gridmap_file
# Description:
#       Loads entire gridmap into a single hash.
#       Single DN may be mapped to multiple users 
#       therefore hash contains lists not scalars.
#       It speeds up later non-sequential access.
#       It detects some errors:
#         - missing double quotes
#         - missing usernames
#         - duplicate entries (logical names)
# Parameters:
#       Hash which is to hold entire gridmap as lines
# Returns:
#       0 gridmap file loaded successfully
#       1 gridmap file loading failed
#       2 if there are duplicate entries
#####################################################
sub load_gridmap_file ( $ )
{	
	$gridmap_handle = new FileHandle ("< $GRID_MAP_FILE");
	
	if (!defined ($gridmap_handle)) 
	{
		return 1;
	}	

	my ($key, $value);
	my $href = shift;
	
	my $duplicate_counter = 0;
		
	my $buffer;
	while (! $gridmap_handle->eof() ) 
	{		
		$buffer = $gridmap_handle->getline();
		chomp ($buffer);
		
		if ($buffer !~ /^\s*$/ && $buffer !~ /^\s*\#/) 
		{
		  $buffer =~ s/^\s*//;
		  $buffer =~ /^"(.*)"\s*(\S*)\s*$/;

          if (! defined ($1)) 
          {
              print "\nERROR: Missing double quotes in the following entry: " . $buffer . "\n";                       
          }
          elsif (! defined ($2)) 
          {
              print "\nERROR: Missing user name(s) in the following entry: " . $buffer . "\n";
          }
          else 
          {
              ($key, $value) = ($1,$2);
          
		      if (exists ($$href{$key}) ) 
		      {
		  	     $duplicate_counter++;
                             $$href{$key} .= ",$value";
		  	     print "\nERROR: Found duplicate entry: " . $buffer . "\n";
		      } 
		      else 
		      {
			    $$href{$key} = $value;
		      }
          }
		}
	}
	if ($duplicate_counter > 0) 
	{
		print "ERROR: Found " . $duplicate_counter . " duplicate(s)\n";		
		return 2;
	}	
	return 0;
}

#####################################################
# Function: check_valid_login_names
# Description:
#       Checks all gridmap entries if user names are
#       valid login names.
#       If invalid user names are found, then each
#       invalid entry is printed.
#       Prints number of invalid entries if this
#       number is non-zero.
# Parameters:
#       Reference to a hash holding gridmap entries
# Returns:
#       0 all entries are valid
#       1 if there are invalid login names
#####################################################
sub check_valid_login_names ( $ ) 
{
	my ($href, $entry, $user, $user_id);
	my @user_names;
	
	my %invalid_login_names = ();
		
	$href = shift;

	foreach $entry (values(%$href)) 
	{
	    if ($entry =~ /,/) 
	    {
	      @user_names = split (/,/, $entry);
	    } 
	    else 
	    {
	      $user_names[0] = $entry;
	    }	    
	    
	    foreach $user (@user_names) 
	    {
		    $user_id = getpwnam("$user");
		    if (! defined ($user_id) || $user_id < 0)  
		    {
			    print "\nERROR: " . $user . " is not a valid local username\n";
			    if (! exists ($invalid_login_names{"$user"}) )
			    {
			        $invalid_login_names{"$user"} = 1;
			    }  
		    }
	    }
	}
	my $counter = keys (%invalid_login_names);
	if ($counter > 0) 
	{
		print "ERROR: Found " . $counter . " invalid username(s)\n";
		return 1;
	} 
	else
	{
		return 0;
	}
}
