#!/usr/bin/perl

#
# nordugridmap - generates grid-mapfile(s) based on configuration
#

binmode STDIN;
binmode STDOUT;

use Getopt::Long;
use Net::LDAP;
use URI;
use XML::DOM;
use LWP::UserAgent;
use LWP::Protocol::https;
use File::Temp qw(tempfile);
use File::Path;
use Storable;
use SOAP::Lite;
use SOAP::Transport::HTTP;
use Crypt::OpenSSL::X509;

# please use this when developing
use warnings;
use strict;

use constant {
    # debug level constatns
    FATAL   => 0,
    ERROR   => 1,
    WARNING => 2,
    INFO    => 3,
    VERBOSE => 4,
    DEBUG   => 5,
    # nordugridmap internals
    VERSION     => "2.1",
    USERAGENT   => "nordugridmap"
};

#
# GET COMMAND LINE OPTIONS AND SET DEFAULTS
#

# define configuration flags 
my %config_flags = (
    'require_issuerdn_used' => 0,
    'generate_vomapfile'    => 1,
    'issuer_processing'     => 0, # strict = 1, relaxed = 0
    'mapuser_processing'    => 0, # overwrite = 1, keep = 0
    'cache_enabled'         => 1,
    'log_to_file'           => 0,
    'voms_use_soap'         => 1, # voms_method: soap = 1, get = 0
    'allow_empty_unixid'    => 0
);

my $debug_level = 2;
my $fetch_url_timeout = 15;
my $opt_help;
my $opt_test;
my $fileopt = $ENV{ARC_CONFIG}||="/etc/arc.conf";

# get options
GetOptions("help" => \$opt_help,
           "test" => \$opt_test,
           "config=s" => \$fileopt);

if ($opt_help) {
    &printHelp;
    exit(1);
}

$debug_level = 5 if $opt_test;
&Logger("Testing mode is enabled using command line option. NO gridmaps will be altered.", DEBUG);

#
# CONFIG FILE PARSER (ARC.CONF INI FORMAT EXPECTED)
#

unless (open (CONFIGFILE, "<$fileopt")) {
    &Logger("Can't open $fileopt configuration file",FATAL);
}

my %parsedconfig = ();
my $blockname;
my $blockindex=0;
my $lineindex=0;

while (my $line =<CONFIGFILE>) {
    $lineindex++;
    next if $line =~/^#/;
    next if $line =~/^$/;
    next if $line =~/^\s+$/;

    # parse block name
    if ($line =~/\[([^\/]+).*\]/ ) {
        $blockindex++;
        $blockname = $1;
        unless ( $blockname =~ /^(common|nordugridmap)$/ ) {
            $blockname = sprintf("%s_%03i",$1,$blockindex);
        }
        $parsedconfig{$blockname}{'configline'} = $lineindex;
        next;
    }

    # skip every block not related to nordugridmap
    next unless ( $blockname =~/^(common|nordugridmap|vo_)/ );

    # get variable = "value"
    next unless ( $line =~/^(\w+)\s*=\s*"(.*)"\s*$/ );
    my $variable_name=$1;
    my $variable_value=$2;

    if ( $blockname =~/^vo_/ ) {
        # special parsing for the local grid-mapfile
        if ($variable_name eq "localmapfile") {
            $variable_name = "source";
            $variable_value = "file://" . $variable_value;
        }

        # special parsing for the nordugrid VO: source="nordugrid"
        if (($variable_name eq "source") && ($variable_value eq "nordugrid")) {
            $variable_value = "vomss://voms.ndgf.org:8443/voms/nordugrid.org";
        }

        # check for 'require_issuerdn' options are being used anywhere
        if ( $variable_name eq "require_issuerdn" ) {
            $config_flags{"require_issuerdn_used"} = 1 if ($variable_value eq "yes");
        }
    }

    # store values to hash: $parsedconfig{blockname[_blockindex]}{variable_name}
    unless ($parsedconfig{$blockname}{$variable_name}) {
        $parsedconfig{$blockname}{$variable_name} = $variable_value;
    } else {
        $parsedconfig{$blockname}{$variable_name} .= '[separator]' . $variable_value;
    }
}

close CONFIGFILE;

# 
# CHECK CONFIGURATION FOR REQUIRED INFO
# 

# check [vo] blocks exists
my @blocknames_tmp = (keys %parsedconfig);
unless ( grep /^vo_/, @blocknames_tmp) {
    &Logger("No [vo] blocks are found in the $fileopt configuration file",FATAL);
}

# general configurable options (order: [nordugridmap] -> [common] -> $ENV -> defaults);
my $capath          = $parsedconfig{"nordugridmap"}{"x509_cert_dir"} ||
                      $parsedconfig{"common"}{"x509_cert_dir"} ||
                      $ENV{X509_CERT_DIR} ||
                      "/etc/grid-security/certificates/";
my $x509cert        = $parsedconfig{'nordugridmap'}{'x509_user_cert'} ||
                      $parsedconfig{'common'}{'x509_user_cert'} ||
                      $ENV{X509_USER_CERT} ||
                      "/etc/grid-security/hostcert.pem";
my $x509key         = $parsedconfig{'nordugridmap'}{'x509_user_key'} ||
                      $parsedconfig{'common'}{'x509_user_key'} ||
                      $ENV{X509_USER_KEY} ||
                      "/etc/grid-security/hostkey.pem";
my $gridvomapfile   = $parsedconfig{'nordugridmap'}{'vomapfile'} || 
                      "/etc/grid-security/grid-vo-mapfile";
my $default_mapfile = $parsedconfig{'nordugridmap'}{'gridmap'} ||
                      $parsedconfig{'common'}{'gridmap'} ||
                      "/etc/grid-security/grid-mapfile";
my $logfile         = $parsedconfig{'nordugridmap'}{'logfile'} ||
                      "/var/log/arc/nordugridmap.log";
my $cachedir        = $parsedconfig{'nordugridmap'}{'cachedir'} ||
                      "/var/spool/nordugrid/gridmapcache/";
my $cache_maxlife   = $parsedconfig{'nordugridmap'}{'cachetime'} ||
                      3 * 24 * 60 * 60; # three days old

&set_numeric_value(\$debug_level, 'debug', '0 to 5') unless $opt_test;
&set_numeric_value(\$fetch_url_timeout, 'fetch_timeout', 'numeric integers');

&set_configuration_flag('cache_enabled','cache_enable','yes','no');
&set_configuration_flag('issuer_processing','issuer_processing','strict','relaxed');
&set_configuration_flag('mapuser_processing','mapuser_processing','overwrite','keep');
&set_configuration_flag('allow_empty_unixid','allow_empty_unixid','yes','no');
&set_configuration_flag('generate_vomapfile','generate_vomapfile','yes','no');
&set_configuration_flag('voms_use_soap','voms_method','soap','get');

&set_configuration_flag('log_to_file', 'log_to_file', 'yes', 'no') unless $opt_test;

#
# ENABLE/DISABLE FEATURES DEPEND ON CONFIGURATION
#

# redirect log to file
if ( $config_flags{'log_to_file'} ) {
    open ( STDERR, ">> $logfile" ) or &Logger("Cannot open logfile '$logfile' for writting. Exiting.", FATAL);
    my $time_string = localtime;
    print STDERR "\n------- nordugridmap started at $time_string -------\n";
}

# if cache enabled ensure cache directory exists and writable
if ( $config_flags{'cache_enabled'} ) {
    # check cachedir exists
    unless ( -d $cachedir ) {
        &Logger("Cache directory does not exists. Trying to create...", WARNING);
        eval { mkpath($cachedir) };
        if ($@) {
            &Logger("Failed to create cache directory $cachedir", FATAL);
        }
        &Logger("Cache directory $cachedir has been created", INFO);
    }
    &Logger("Cache directory $cachedir is not writable", FATAL) unless -w $cachedir;
}

#
# PROCESS [VO] BLOCKS DEPENDENCIES
#

# generate a list of all external sources to fetch
# generate a list of [vo] blocks dependencies
my %sources_list = (); 
my %sources_deps = ();
my %generated_blocks = ();

# process blocks defined in arc.conf
foreach my $block (sort(keys %parsedconfig)) {
    next unless $block =~ /^vo_/;
    my $voname = &get_vo_name($block);
    $sources_deps{"vo://".$voname} = &get_block_sources($block, \%sources_list, \%generated_blocks);
}

# process blocks generated authomaticaly on the previous step
foreach my $block (sort(keys %generated_blocks)) {
    $sources_deps{"vo://".$block} = &get_block_sources($block, \%sources_list, \%generated_blocks, \%generated_blocks);
    $parsedconfig{$block} = $generated_blocks{$block};
}

# ensure loop-free configuration
my %dryrun_sources_data = %sources_list;
&process_vo_blocks(\%sources_deps, \%dryrun_sources_data, 1);

#
# FETCH SOURCES AND ASSEMBLE GRIDMAPS
#

# if require_issuerdn used create CA DN's hash 
my %ca_list = ();
if ( $config_flags{'require_issuerdn_used'} ) {
    &Logger("Issuer DN check is requested in the configuration. Building supported CA list.", DEBUG);
    &create_ca_list(\%ca_list);
}

# fetch all sources 
my %sources_data = ();
&fetch_sources(\%sources_list, \%sources_data);

# assemble [vo] blocks gridmap lists
&process_vo_blocks(\%sources_deps, \%sources_data);

# assemble gridmapfiles
my %mapfile_data = ();
&process_mapfiles(\%mapfile_data, \%sources_data);

# assemble vomapfile 
if ( $config_flags{'generate_vomapfile'} ) {
    &process_vo_mapfile($gridvomapfile, \%mapfile_data, \%sources_data);
}

# write mapfiles to disk
if ( $opt_test ) {
    &write_mapfiles_data(\%mapfile_data, 1);
} else {
    &write_mapfiles_data(\%mapfile_data);
}

# END OF MAIN ROUTINE :-)

#
# GENERAL CONFIGURATION PARSER SUBROUTINES
#

# get VO name for [vo] block
sub get_vo_name {
    my $block = shift;
    if ( $parsedconfig{$block}{'vo'} ) {
        return $parsedconfig{$block}{'vo'};
    } else {
        &Logger("Cannot get the VO name: parameter 'vo' is required but not set. Please check [vo] block configuratinon ($fileopt line $parsedconfig{$block}{'configline'})", FATAL);
    }
}

# set configuration flags in %config_flags based on [nordugridmap] parsed configuration
sub set_configuration_flag {
    my ( $flag_name, $option_name, $value_yes, $value_no ) = @_;
    if ( defined $parsedconfig{'nordugridmap'}{$option_name} ) {
        if ( $parsedconfig{'nordugridmap'}{$option_name} eq $value_yes ) {
            $config_flags{$flag_name} = 1;
        } elsif ( $parsedconfig{'nordugridmap'}{$option_name} eq $value_no ) {
            $config_flags{$flag_name} = 0;
        } else {
            my $text_def = $config_flags{$flag_name} ? $value_yes : $value_no;
            &Logger("Unrecognized value for option '$option_name' in [nordugridmap] configuration. Valid valueas are: '$value_yes' or '$value_no'. Using default '$text_def'", WARNING);
        }
    }
}

# return numeric value of [nordugridmap] parsed configuration option
sub set_numeric_value {
    my ( $ref_var, $option_name, $value_valid ) = @_;

    if ( defined $parsedconfig{'nordugridmap'}{$option_name} ) {
        if ( $parsedconfig{'nordugridmap'}{$option_name} =~ /^\d+$/ ) {
            $$ref_var = $parsedconfig{'nordugridmap'}{$option_name};
        } else {
            &Logger("Unrecognized value for option '$option_name' in [nordugridmap] configuration. Valid valueas are: $value_valid. Using default value: $$ref_var.", WARNING);
        }
    } 
}

# return boolean flag value in specified %options_hash
sub get_source_flag {
    my ( $ref_options_hash, $flag_name, $option_name, $value_yes, $value_no ) = @_;
    if ( defined $ref_options_hash->{$option_name} ) {
        return 1 if ( $ref_options_hash->{$option_name} eq $value_yes );
        return 0 if ( $ref_options_hash->{$option_name} eq $value_no );
        my $text_def = $config_flags{$flag_name} ? $value_yes : $value_no;
        &Logger("Unrecognized value for source-specific option '$option_name'. Valid valueas are: '$value_yes' or '$value_no'. Using globaly configured value '$text_def'", WARNING);
    }
    return $config_flags{$flag_name};
}

# return parsed edg-mkgridmap configuration hash 
sub parse_edg_mkgridmap_conf {
    my $file = shift;
    my %confighash = ();

    unless (open (EDGMKGRIDMAPCONF, "<$file")) {
        &Logger("Can't open $file edg-mkgridmap configuration file", ERROR);
        return (1, \%confighash);
    }

    &Logger("Parsing edg-mkgridmap configuration file: $file", INFO);
    my $exit_code = 2;
    while (my $line =<EDGMKGRIDMAPCONF>) {
        next if $line =~/^#/;
        next if $line =~/^\s*$/;

        my ( $keyword, $arg1, $dummy, $arg2 ) = $line =~ /^\s*(\w+)\s+([^\s]+)(\s+([^\s]+))?$/;
        my $conf_key = 0;
        my $conf_value = 0;

        unless ( defined $arg1 ) {
            &Logger("  edg-mkgridmap parser: Argument is required in line '$line', ignoring.", WARNING);
            next;
        }

        if ( $keyword eq 'group' ) {
            # convert to external sources
            $conf_key = 'source';
            $conf_value = $arg1;
            if ( defined $arg2 ) {
                if ( $arg2 eq 'AUTO' ) { 
                    &Logger("  edg-mkgridmap parser: Ignoring unsupported AUTO mapping for source $conf_value", VERBOSE);
                } else {
                    $conf_value .= " < mapped_unixid=$arg2";
                }
            }
            $exit_code = 0;
        } elsif ( $keyword eq 'default_lcluser' ) {
            # convert to mapped_unixid
            &Logger("  edg-mkgridmap parser: adding mapped_unixid=$arg1", DEBUG);
            $confighash{'mapped_unixid'} = $arg1;
        } elsif ( $keyword eq 'auth' ) {
            # checking subjects over LDAP auth server is unsupported by nordugridmap
            &Logger("  edg-mkgridmap parser: Ignoring unsupported 'auth' keyword", VERBOSE);
        } elsif ( $keyword =~ m/^(allow|deny)$/ ) {
            # confert to filter
            $conf_key = 'filter';
            chomp($line);
            $conf_value = $line;
        } elsif ( $keyword eq 'gmf_local' ) {
            # convert to file source
            $conf_key = 'source';
            $conf_value = "file://" . $arg1;
            $exit_code = 0;
        } else {
            &Logger("  edg-mkgridmap parser: Ignoring unrecognized keyword for directive '$line'", WARNING);
        }

        if ( $conf_key ) {
            &Logger("  edg-mkgridmap parser: adding $conf_key record $conf_value", DEBUG);
            if ( defined $confighash{$conf_key} ) {
                $confighash{$conf_key} .= '[separator]' . $conf_value;
            } else {
                $confighash{$conf_key} = $conf_value;
            }
        }
    }
    
    close EDGMKGRIDMAPCONF;
    # no mapfile assembling required for generated [vo] block
    $confighash{'file'} = '/dev/null';
    return ($exit_code, \%confighash);
}

#
# MAPPING PROCESSING SUBROUTINES
#

# assemble vo-mapfile data
sub process_vo_mapfile {
    my ( $gvmf, $ref_mapfile_data, $ref_sources_data ) = @_;

    &Logger("Assembling vo-mapfile mapping list", INFO);
    $ref_mapfile_data->{$gvmf} = {};

    foreach my $block (sort(keys %parsedconfig)) {
        next unless $block =~ /^vo_/;
        my $vo_ref = "vo://" . &get_vo_name($block);

        foreach my $dn ( keys %{$ref_sources_data->{$vo_ref}} ) {
            unless ( defined $ref_mapfile_data->{$gvmf}->{$dn} ) {
                $ref_mapfile_data->{$gvmf}->{$dn} = '"' . $ref_sources_data->{$vo_ref}->{$dn}->{'source'} . '"';
            }
        }
    }
}

# assemble grid-mapfiles data
sub process_mapfiles {
    my ( $ref_mapfile_data, $ref_sources_data ) = @_;

    foreach my $block (sort(keys %parsedconfig)) {
        next unless $block =~ /^vo_/;
        my $gmf = $parsedconfig{$block}{'file'} || $default_mapfile;

        next if $gmf eq '/dev/null';
        next if defined $ref_mapfile_data->{$gmf};

        &Logger("Assembling gridmap file: $gmf", INFO);
        $ref_mapfile_data->{$gmf} = {};

        my @vo_blocks_list = &get_file_vo_sources($gmf);
        foreach my $source ( @vo_blocks_list ) {
            foreach my $dn ( keys %{$ref_sources_data->{$source}} ) {
                unless ( defined $ref_mapfile_data->{$gmf}->{$dn} ) {
                    $ref_mapfile_data->{$gmf}->{$dn} = $ref_sources_data->{$source}->{$dn}->{'mapuser'};
                } else {
                    &Logger("Entry '$dn' already exists in $gmf gridmapfile. Skiped.", DEBUG);
                }
            }
        }
    }
}

# asseble [vo] blocks mapping data
sub process_vo_blocks {
    my ($ref_sources_deps, $ref_sources_data, $dryrun) = @_;

    my $blocks_unfinished = 1;
    my $blocks_processed = 1;

    # loop until all [vo] blocks are processed
    while ( $blocks_unfinished ) {
        if ( $blocks_processed == 0 ) {
            &Logger("Loop detected in the [vo] blocks dependencied. Please review you configuration.", FATAL);
        }

        # initial values
        $blocks_unfinished = 0;
        $blocks_processed = 0;

        foreach my $block (sort(keys %parsedconfig)) {
            next unless $block =~ /^vo_/;

            my $vo_name = &get_vo_name($block);
            my $vo_ref = "vo://" . $vo_name;
            next if defined $ref_sources_data->{$vo_ref};
            $blocks_unfinished++;

            # check all sources fetched or already assembled
            my $undefined_cnt = 0;
            foreach my $source ( @{$ref_sources_deps->{$vo_ref}} ) {
                $undefined_cnt++ unless defined $ref_sources_data->{$source};
            }

            # assemble [vo] block gridmap
            unless ( $undefined_cnt ) {
                unless ( $dryrun ) {
                    # get [vo] block parameters
                    my $require_issuerdn = "no";
                    my $mapped_user = "";

                    if ( $parsedconfig{$block}{'require_issuerdn'} ) {
                        $require_issuerdn = $parsedconfig{$block}{'require_issuerdn'};
                    }
                    if ( $parsedconfig{$block}{'mapped_unixid'} ) {
                        $mapped_user = $parsedconfig{$block}{'mapped_unixid'};
                    }

                    # define [vo] block filter if any
                    my @Rules = ();
                    if ( $parsedconfig{$block}{'filter'} ) {
                        my @filters = split /\[separator\]/, $parsedconfig{$block}{'filter'};
                        foreach my $filter_entry (@filters) {
                            push @Rules, $filter_entry;
                        }
                        # if we allow certain people, deny becomes last option
                        if ( ($parsedconfig{$block}{'filter'} =~ /allow/) ) {
                            push @Rules, "deny *";
                        }
                    } else {
                        # no filters - allow all
                        push @Rules, "allow *";
                    }

                    # print block parameters summary on debug
                    my $rules_str = join("\n                           ",@Rules);
                    &Logger("Assembling gridmap list for [vo] block:
        VO BLOCK NAME    : $vo_name
        ACL              : $rules_str
        MAPPED UNIXID    : $mapped_user
        REQUIRE_ISSUERDN : $require_issuerdn", DEBUG);

                    # process all sources
                    $ref_sources_data->{$vo_ref} = {};
                    foreach my $source ( @{$ref_sources_deps->{$vo_ref}} ) {
                        foreach my $dn ( keys %{$ref_sources_data->{$source}} ) {
                            my %source_dn_hash = %{$ref_sources_data->{$source}->{$dn}};
                            unless ( defined $ref_sources_data->{$vo_ref}->{$dn} ) {
                                # check DN is filtered
                                next unless &rule_match($dn, \@Rules);
                                # check ISSUERDN if requested
                                if ( $require_issuerdn eq "yes" ) {
                                    if ( defined $source_dn_hash{'issuer'} ) {
                                        next unless &issuer_match($dn, $source_dn_hash{'issuer'}, \%ca_list);
                                    } else {
                                        # on 'strict' issuer processing issuer dn is mandatory
                                        if ( $config_flags{'issuer_processing'} ) {
                                            &Logger("User '$dn' denied due to strict issuer processing - there is no issuer dn", DEBUG);
                                            next;
                                        }
                                    }
                                }
                                # check mapping user exists for record
                                if ( $config_flags{'mapuser_processing'} || ! defined $source_dn_hash{'mapuser'} ) {
                                    if ( $mapped_user eq "" ) {
                                        unless ( $config_flags{'allow_empty_unixid'} ) {
                                            &Logger("There is no mapping for DN '$dn' in [vo] block $vo_name. Skiping record.", WARNING);
                                            next;
                                        } else {
                                            &Logger("Using empty mapping for DN '$dn' in [vo] block $vo_name.", VERBOSE);
                                        }
                                    }
                                }

                                # if we are still here - add entry
                                $ref_sources_data->{$vo_ref}->{$dn} = \%source_dn_hash;
                                # always map to common user on 'rewrite' mapuser processing
                                if ( $config_flags{'mapuser_processing'} || ! defined $ref_sources_data->{$vo_ref}->{$dn}->{'mapuser'} ) {
                                        $ref_sources_data->{$vo_ref}->{$dn}->{'mapuser'} = $mapped_user;
                                }
                                &Logger("Adding entry '$dn -> $ref_sources_data->{$vo_ref}->{$dn}->{'mapuser'}' to [vo] block '$vo_name' gridmap.", DEBUG);
                                # maintain information about where record is come from
                                unless ( defined $ref_sources_data->{$vo_ref}->{$dn}->{'source'} ) {
                                    $ref_sources_data->{$vo_ref}->{$dn}->{'source'} = $source;
                                }
                            } else {
                                &Logger("Entry '$dn' already exists in [vo] block '$vo_name' gridmap. Skiped.", DEBUG);
                            }
                        }
                    }
                } else {
                    $ref_sources_data->{$vo_ref} = 1;
                }
                $blocks_processed++;
            }
        }
    }
}

# write mapfiles to disk
sub write_mapfiles_data {
    my ( $ref_mapfile_data, $dryrun) = @_;

    foreach my $mapfile ( keys %$ref_mapfile_data ) {
        unless ( $dryrun ) {
            my ($gmf, $tmp_mapfile) = tempfile($mapfile . "XXXXX", UNLINK => 1) or 
                &Logger("Cannot open temporary file to write $mapfile data", FATAL);

            &Logger("Writting mapfile data for $mapfile", INFO);

            while ( my ($dn, $map) = each(%{$ref_mapfile_data->{$mapfile}}) ) {
                print $gmf "\"$dn\" $map\n" or &Logger("Failed to write gridmap data (not enough disk space?) to $tmp_mapfile", FATAL);
            }

            close($gmf);
            rename $tmp_mapfile, $mapfile;
        } else {
            my $gmf_string = "";
            while ( my ($dn, $map) = each(%{$ref_mapfile_data->{$mapfile}}) ) {
                $gmf_string .= "    \"$dn\" $map\n";
            }
            &Logger("Printing mapfile content for $mapfile:\n$gmf_string", INFO);
        }
    }
}

#
# SOURCES DEPENDENCIES TRACKING SUBROTINES
#

# return array of [vo] blocks required to generate gridmapfile
sub get_file_vo_sources {
    my $file_name = shift;
    my @file_vos_list = ();

    foreach my $block (sort(keys %parsedconfig)) {
        next unless $block =~ /^vo_/;
        if ( defined $parsedconfig{$block}{'file'} ) {
            next unless $parsedconfig{$block}{'file'} eq $file_name;
        } else {
            next unless $file_name eq $default_mapfile;
        }
        push @file_vos_list, "vo://".get_vo_name($block);
    }

    return @file_vos_list;
}

# extract optional per-source parameters from source string and return hash
# optional parameters will be removed from passed source string
sub get_source_params {
    my $ref_source = shift;
    my ( $source_str, $params_str ) = split '<', $$ref_source;
    # trim url without optional parameters and return back
    $source_str =~ s/^\s+//;
    $source_str =~ s/\s+$//;
    $$ref_source = $source_str;
    &Logger("  source URL: $source_str", DEBUG);
    # create source parameters hash
    my %source_params = ();
    if ( defined $params_str ) {
        foreach my $param_record ( split ' ', $params_str ) {
            next unless ( $param_record =~/^(\w+)=(.+)$/ );
            &Logger("  source optional parameter '$1'=$2", DEBUG);
            $source_params{$1}=$2;
        }
    }
    return \%source_params;
}

# return list of block dependencied and fill external sources list
sub get_block_sources {
    my ($block_id, $ref_sources_list, $ref_generated_blocks, $ref_confighash) = @_;
    # parsed arc.conf hash is used by default
    $ref_confighash = \%parsedconfig unless defined $ref_confighash;
    # array with block dependencied
    my @vo_sources_list = ();

    &Logger("Getting sources for VO block: $block_id", DEBUG);
    my @urls = split /\[separator\]/, $ref_confighash->{$block_id}{'source'};
    foreach my $source (@urls) {
        &Logger("Found mapping source record: $source", DEBUG);
        # get optional per-source parameters
        my $ref_source_params = &get_source_params(\$source);
        my $source_id = $source;
        # check sourcs is already in sources list
        if ( defined $ref_sources_list->{$source} ) {
            # if source parameters differ - use block_id prefix
            if ( &Storable::freeze($ref_source_params) ne &Storable::freeze($ref_sources_list->{$source}) ) {
                &Logger("  duplicate source URL with different parameters: adding block ID prefix", DEBUG);
                $source_id = "$block_id|$source";
            } else {
	            &Logger("  source URL is already defined", DEBUG);
            }
        }
        # get source protocol
        my ( $protocol, $uri ) = $source =~ m/([-\w]+):\/\/(.*)/;
        $protocol = lc $protocol;
        # process URLs depend on protocol used
        if ( $protocol =~ /^vomss?$/i ) {
            # special handling for voms_fqan_map
            if ( defined $ref_confighash->{$block_id}{'voms_fqan_map'} ) {
                # FQANs defined for VOMS URL: generate URL for every FQAN
                my @fqans = split /\[separator\]/, $ref_confighash->{$block_id}{'voms_fqan_map'};
                my ( $voms_baseid, $dummy_fqan ) = $source_id =~ m/^([^\?]+)\??(.*)$/;
                foreach my $fqan_match ( @fqans ) {
                    my ( $fqan, $map_id ) = $fqan_match =~ m/^([^\s]+)\s+(.*)$/;
                    # create URL with specified FQAN
                    my $fqan_source_id = $voms_baseid . "?" . $fqan;
                    my ( $dummy_id, $fqan_source_url ) = $fqan_source_id =~ m/(\w+\|)?([^|]+)/;
                    &Logger("Generating FQAN-map source URL: $fqan_source_url (mapped to $map_id)", VERBOSE);
                    # put mapped_unixid parameter
                    my %fqan_source_params = %$ref_source_params;
                    $fqan_source_params{'mapped_unixid'} = $map_id;
                    # save as [vo] block source
                    $ref_sources_list->{$fqan_source_id} = \%fqan_source_params;
                    push @vo_sources_list, $fqan_source_id;
                }
            }
            # standalone VOMS URL: retreive and use directly as VO source
            # FQANs before original URL to apply specific maps first
            $ref_sources_list->{$source_id} = $ref_source_params;
            push @vo_sources_list, $source_id;

        } elsif ( $protocol =~ /^(https?|ldap)$/i ) {
            # external sources: retreive and use directly as VO source
            $ref_sources_list->{$source_id} = $ref_source_params;
            push @vo_sources_list, $source_id;
        } elsif ( $protocol =~ /^file$/i ) {
            # local file: if created by nordugridmap - use [vo] blocks as VO sources
            #             if file is independent source - use directly
            my @file_vo_sources = &get_file_vo_sources($uri);
            if ( @file_vo_sources ) {
                push @vo_sources_list, @file_vo_sources;
            } else {
                if ( -e $uri ) {
                    $ref_sources_list->{$source_id} = $ref_source_params;
                    push @vo_sources_list, $source_id;
                } else {
                    &Logger("File source '$uri' does not exists. Ignoring.", WARNING);
                }
            } 
        } elsif ( $protocol =~ /^vo$/i ) {
            # [vo] block: use directly as VO source
            push @vo_sources_list, $source_id;
        } elsif ( $protocol =~ /^edg-mkgridmap$/i ) {
            # generate edg-mkgridmap block ID
            my $emkgm_id = "vo_edgmkgm$uri";
            $emkgm_id =~ s/[^a-zA-Z0-9]/_/g;
            # if alredy parsed, nothing to do
            unless ( defined $ref_generated_blocks->{$emkgm_id} ) {
                # parse egd-mkgridmap configuration file
                my ( $exit_code, $ref_parsed_confighash ) = &parse_edg_mkgridmap_conf($uri);
                if ( $exit_code ) {
                    &Logger("Filed to parse edg-mkgridmap config file $uri", ERROR);
                } else {
                    # create [vo] block (with parameters specified in egd-mkgridmap.conf)
                    $ref_generated_blocks->{$emkgm_id} = $ref_parsed_confighash;
                    $ref_generated_blocks->{$emkgm_id}{'vo'} = $emkgm_id;
                    # use created [vo] block as a source
                    push @vo_sources_list, "vo://$emkgm_id";
                }
            }
        } else {
            &Logger("Unsupported protocol found: $source", WARNING);
        }
    }
    return \@vo_sources_list;
}


#
# SUBROUTINES TO GET INFORMATION FROM DIFFERENT SOURCES
#

# fetch data from all sources in sources_list and put them to sources_data hash
sub fetch_sources {
    my ( $ref_sources_list, $ref_sources_data ) = @_;
    my $exit_code;
    my $ref_subjects;

    foreach my $source_id (keys %$ref_sources_list) {
        # separate optional block_id prefix from source URL
        my ( $block_id, $source ) = $source_id =~ m/(\w+\|)?([^|]+)/;
        # get source parameters
        my ( $protocol, $uri ) = $source =~ m/(\w+):\/\/(.*)/;
        my $ref_source_params = $ref_sources_list->{$source_id};
        # check source-specific cache control
        my $use_cache = &get_source_flag($ref_source_params, 'cache_enabled','cache_enable','yes','no');
        # get subjects from external URL
        if ( $protocol =~ /^vomss?$/i ) {
            ($exit_code, $ref_subjects) = &voms_subjects($source, $ref_source_params);
        } elsif ( $protocol =~ /^https?$/i ) {
            ($exit_code, $ref_subjects) = &http_subjects($source, $ref_source_params);
        } elsif ( $protocol =~ /^ldap$/i ) {
            ($exit_code, $ref_subjects) = &ldap_subjects($uri, $ref_source_params);
        } elsif ( $protocol =~ /^file$/i ) {
            ($exit_code, $ref_subjects) = &read_gridmap($uri, $ref_source_params);
        } else {
            &Logger("Unsupported protocol to fetch: $protocol", FATAL);
        }
        # check fetch result and try to save/load cache
        unless ( $exit_code ) {
            if ( $use_cache ) {
                &write_cached_subjects($source_id, $ref_subjects) unless $opt_test;
            }
        } else {
            &Logger("Fail to retreive data from URL: $source", WARNING);
            if ( $use_cache ) {
                my ($err_code, $cache_ref_subjects) = &read_cached_subjects($source_id);
                unless ($err_code) {
                    &Logger("Using locally cached data for URL: $source", INFO);
                    $ref_subjects = $cache_ref_subjects;
                }
            }
        }
        # put fetched results to sources_data hash
        $ref_sources_data->{$source_id} = $ref_subjects;
    }
}

# setup HTTPS SSL parameters
sub setup_https {
    # For Net::SSL
    $ENV{HTTPS_CERT_FILE} = $x509cert;
    $ENV{HTTPS_KEY_FILE}  = $x509key;
    $ENV{HTTPS_CA_DIR} = $capath;
    # For IO::Socket::SSL (LWP)
    if ( $IO::Socket::SSL::VERSION ) {
        IO::Socket::SSL::set_ctx_defaults(
            ca_path => $capath,
            use_cert => 1,
            key_file => $x509key,
            cert_file => $x509cert,
            verify_mode => 1
        );
    }
}

# get content of HTTP(S) URL
sub get_http_url {
    my $uri = shift;
    my $scheme = $uri->scheme;
    &Logger("Unsupported URL ($uri) passed to method", FATAL) unless ( $scheme =~ /^https?$/ );

    # handle SSL environment
    &setup_https() if ($uri->scheme eq 'https');

    # create LWP object
    my $ua = LWP::UserAgent->new( agent => USERAGENT."/".VERSION,
                                  timeout => $fetch_url_timeout );
    # do GET query
    my $res = $ua->get($uri,
                    'Cache-Control' => 'no-cache',
                    'Pragma'        => 'no-cache');

    unless ($res->is_success) {
        &Logger("HTTP request failed for URL $uri:\n\t". $res->message, ERROR);
        return 0;
    }

    return $res->content;
}

# HTTP(S) sources: expect plain text list of "DN" "CA"
sub http_subjects {
    my ($url, $ref_source_params) = @_;
    my %Subjects = ();
   
    # get subjects from URL specified
    &Logger("Getting subjects from source: $url", DEBUG);
    my $uri = URI->new($url);
    my $content = get_http_url($uri);
    unless ($content) {
       &Logger("Failed to get information from source: $url", ERROR);
       return (1, \%Subjects);
    }

    my $count = 0;
    foreach my $line ( split /\n/, $content ) {
        next if $line =~ /^(\s)*$/;
        chomp($line);

        # get "subject" "issuer" pairs
        my ($subject, $issuer) = split (/\s+"(.*)"/, $line);
        $subject =~ s/"(.*)"/$1/g;
        $issuer =~ s/"(.*)"/$1/g if $issuer;

        $Subjects{$subject} = { 'subject'    => $subject,
                                'issuer'     => $issuer };

        # mapped_unixid can be passed via optional parameters
	$Subjects{$subject}{'mapuser'} = $ref_source_params->{'mapped_unixid'} if defined $ref_source_params->{'mapped_unixid'};

        $count++;
    }

    &Logger("No information retreived from URL: $url", WARNING) unless $count;
    return (0, \%Subjects);
}

# LDAP sources: expect LDAP-schema formated VO Group
sub ldap_subjects {
    my ($hostport, $ref_source_params) = @_;
    my %Subjects = ();

    &Logger("Getting subjects from source: ldap://$hostport", DEBUG);

    my ($host, $port) = split /:/, $hostport;
    if (! defined $port) { $port=389 }
    if ($port eq "") { $port=389 }
    my $ldap = Net::LDAP ->new($host, port => $port, timeout => $fetch_url_timeout );
    if ( $@ ) {
        &Logger("VO Group ldap://$host is unreachable:\n $@", ERROR);
        return (1, \%Subjects);
    }

    my $base;
    my $mesg = $ldap->search(base => $base,
                          timelimit => 120,
                          filter => "member=*");
    &Logger($mesg->error . " with $base", WARNING) if $mesg->code;

    foreach my $groupServer ($mesg->all_entries) {
        my $dn = $groupServer->dn();
        my @allMembers = $groupServer->get_value('member');
        &Logger("Entries from the GroupDN: $dn", DEBUG);
        
        foreach my $member (@allMembers){
            my $mesg2 = $ldap->search(base => $member,
                                      timelimit => 120,
                                      filter => "cn=*");
            if ( $mesg2->code ) {
                &Logger($mesg2->error . " with $base", ERROR);
                return (1, \%Subjects);
            }
            my $entry = $mesg2->entry(0);

            if (!$entry){
                &Logger("\"$member\" not found", WARNING);
                next;
            }

            my $subj = "";
            my @Subj = $entry->get_value('description');
            my $cn = $entry->get_value('cn');
            my $issuer = $entry->get_value('nordugrid-issuerDN');

            foreach $_ (@Subj) {
                if($_ =~ /^subject=\s*(.*)/){
                    $subj = $1;
                    last;
                }
            }
            if ($subj eq ""){
                &Logger("\"subject=\" not found in description of $cn", WARNING);
                next;
            }

            $Subjects{$subj} = { 'subject' => $subj,
                                 'issuer'  => $issuer };

            # mapped_unixid can be passed via optional parameters
            $Subjects{$subj}{'mapuser'} = $ref_source_params->{'mapped_unixid'} if defined $ref_source_params->{'mapped_unixid'};
        }
    }
    return (0, \%Subjects);
}

# VOMS(S) methods wrapper
sub voms_subjects {
    my ($url, $ref_source_params) = @_;
    my $use_soap = &get_source_flag($ref_source_params, 'voms_use_soap', 'voms_method', 'soap', 'get');

    if ( $use_soap ) {
        return &voms_subjects_soap($url, $ref_source_params);
    } else {
        return &voms_subjects_get($url, $ref_source_params);
    }
}

# VOMS(S) sources: expect VOMS-Admin SOAP responce (SOAP:Lite implementation)
sub voms_subjects_soap {
    my ($url, $ref_source_params) = @_;
    my %Subjects = ();

    &Logger("Getting subjects from source: $url", DEBUG);

    # get SOAP endpoint URL and container
    my ( $endpoint, $container ) = split(/\?/, $url, 2);
    $endpoint =~ s/^voms/http/;

    # handle SSL environment
    &setup_https() if $endpoint =~ /^https/;

    $endpoint .= '/services/VOMSCompatibility';
    my $soap_client;
    eval {
        $soap_client = SOAP::Lite->proxy($endpoint,
                                        agent => USERAGENT."/".VERSION,
                                        timeout => $fetch_url_timeout );
    };
    unless ( $soap_client ) {
        &Logger("Failed to connect to SOAP endpoint: $url", ERROR);
        return (1, \%Subjects);
    }

    # call getGridmapUsers method
    my $soap_req;
    eval { 
        if ( $container ) {
            $soap_req = $soap_client->getGridmapUsers($container);
        } else {
            $soap_req = $soap_client->getGridmapUsers();
        }
    };

    unless ( $soap_client->transport->is_success ) {
        &Logger("SOAP transport failed for URL: $url. Error: ".$soap_client->transport->status, ERROR);
        return (1, \%Subjects);
    }

    unless ($soap_req) {
        &Logger("SOAP responce parsing failed for URL: $url", ERROR);
        return (3, \%Subjects);
    }

    if ( $soap_req->fault ) {
        &Logger("SOAP request failed for URL: $url. Returned error: ".$soap_req->faultstring, ERROR);
        return (4, \%Subjects);
    }

    if ( ref($soap_req->result) ne 'ARRAY' ) {
        &Logger("SOAP returned non-array result for URL: $url", VERBOSE);
        return (0, \%Subjects);
    }

    if ( ! @{$soap_req->result} ) {
        &Logger("SOAP returned empty result for URL: $url", VERBOSE);
        return (0, \%Subjects);
    }

    foreach my $subject ( @{$soap_req->result} ) {
        $Subjects{$subject} = { 'subject' => $subject };
        # mapped_unixid can be passed via optional parameters
        $Subjects{$subject}{'mapuser'} = $ref_source_params->{'mapped_unixid'} if defined $ref_source_params->{'mapped_unixid'};
    }

    return (0, \%Subjects);
}

# VOMS(S) sources: expect VOMS-Admin SOAP responce (GET+XML manual parser implementation)
sub voms_subjects_get {
    my ($url, $ref_source_params) = @_;
    my %Subjects = ();

    &Logger("Getting subjects from source: $url", DEBUG);

    # create proper HTTP(S) URL
    my $uri = URI->new($url);
    my $scheme = $uri->scheme;
    $scheme =~ s/^voms/http/;
    $uri->scheme($scheme);

    # prepare GET query
    $uri->path($uri->path.'/services/VOMSCompatibility');
    if ( $uri->query() ) {
        $uri->query_form( method     => 'getGridmapUsers',
                          container  => $uri->query() );
    } else {
        $uri->query_form( method     => 'getGridmapUsers');
    }

    # get URI content
    my $content = get_http_url($uri);
    return ( 1, \%Subjects) unless $content;

    # parse result on success
    my $parser = new XML::DOM::Parser;
    my $doc;
    eval { $doc = $parser->parse($content) };

    unless ($doc) {
        &Logger("Parsing VOMS ($url) XML response FAILED", ERROR);
        return ( 3, \%Subjects);
    }

    my $retval = $doc->getElementsByTagName('soapenv:Body');
    my $subject;
    if ($retval->getLength == 1) {
        my $returnNode = $doc->getElementsByTagName('getGridmapUsersReturn')->item(0);
        for my $user ($returnNode->getChildNodes) {
            if ($user->getNodeType == ELEMENT_NODE) {
                $subject = undef;
                eval { $subject = $user->getFirstChild->getData };
                if ( defined $subject ) {
                    $Subjects{$subject} = { 'subject' => $subject };
                    # mapped_unixid can be passed via optional parameters
                    $Subjects{$subject}{'mapuser'} = $ref_source_params->{'mapped_unixid'} if defined $ref_source_params->{'mapped_unixid'};
                } else {
                    &Logger("Found subject that cannot be parsed from VOMS XML ($url)", ERROR);
                }
            }
        }
    } else {
        &Logger("VOMS search($uri): No such object", ERROR);
        return ( 4, \%Subjects);
    }

    $doc->dispose;

    return (0, \%Subjects);
}

# Mapfile sources: expect local gridmap-file
sub read_gridmap {
    my ($gridmap_file, $ref_source_params) = @_;
    my %Subjects = ();

    &Logger("Getting subjects from source: file://$gridmap_file", DEBUG);

    if (! -e $gridmap_file) {
        &Logger("File $gridmap_file not found", ERROR);
        return (1, \%Subjects);
    }
    if (! -T $gridmap_file) {
        &Logger("File $gridmap_file not in text format", ERROR);
        return (2, \%Subjects);
    }

    unless (open(IN, "< $gridmap_file")) {
        &Logger("Unable to open $gridmap_file", ERROR);
        return (3, \%Subjects);
    }
    binmode IN;

    # mapped_unixid can be passed via optional parameters, overwriting is controlled by 'mapuser_processing' option
    my $def_mapuser = ( defined $ref_source_params->{'mapped_unixid'} ) ? $ref_source_params->{'mapped_unixid'} : 0;
    my $map_overwrite = &get_source_flag($ref_source_params, 'mapuser_processing','mapuser_processing','overwrite','keep');

    while (my $f = <IN>) {
        chomp($f);

        if ($f =~ /^\s*\"((\/[^\/]+)+)"\s+([^\s]+)\s*$/) {
            # record match: "/user/DN" mapping
            my $subject = $1;
            my $mapuser = $3;
            $mapuser = $def_mapuser if ( $def_mapuser && $map_overwrite );
            $Subjects{$subject} = { 'subject' => $subject,
                                    'mapuser' => $mapuser };
        } elsif ($f =~ /^\s*\"((\/[^\/]+)+)\"\s*$/) {
            # record match: "/user/DN/only"
            my $subject = $1;
            $Subjects{$subject} = { 'subject' => $subject };
            $Subjects{$subject}{'mapuser'} = $def_mapuser if ( $def_mapuser );
        } elsif ($f =~ /^\s*((\/[^\/\s]+)+)\s+([^\s]+)\s*$/) {
            # record match: /user/DN/no_spaces mapping
            my $subject = $1;
            my $mapuser = $3;
            $mapuser = $def_mapuser if ( $def_mapuser && $map_overwrite );
            $Subjects{$subject} = { 'subject' => $subject,
                                    'mapuser' => $mapuser };
        } elsif ($f =~ /^\s*((\/[^\/\s]+)+)\s*$/) {
            # record match: /user/DN/no_spaces/only
            my $subject = $1;
            $Subjects{$subject} = { 'subject' => $subject };
            $Subjects{$subject}{'mapuser'} = $def_mapuser if ( $def_mapuser );
        } else { 
            &Logger("Skipping missformed record '$f' in file $gridmap_file", WARNING);
        }
    }
    close(IN);
    return (0, \%Subjects);
}

#
# MATCHING AND FILTERING 
#

# check subject match against ACL rules
sub rule_match {
    my ($subj, $ref_Rules) = @_;
    my @Rules = @$ref_Rules;

    my $subjReg = $subj;
    $subjReg =~ s/\@/\\\@/g;

    foreach my $rule (@Rules) {
        my ($action, $acl) = split / /, $rule, 2;
        $acl =~ s/\@/\\\@/g;
        $acl =~ s/\*/.\*/g;
        if ($subjReg =~ /$acl/) {
            if ($action eq "deny") {
                &Logger("User '$subj' denied by rule 'deny $acl'", DEBUG);
            } else {
                &Logger("User '$subj' allowed by rule 'allow $acl'", DEBUG) if ( $acl ne ".*" );
                return 1;
            }
            last;
        }
    }
    return 0;
}

# create issuer dn list
sub create_ca_list {
    my $ref_ca_list = shift;

    # get list of CA public key files
    opendir(CADIR, $capath) or &Logger("Cannot open CA directory $capath", FATAL);
    my $ca_list_string = "";
    while (my $file = readdir(CADIR)) {
        next if ($file !~ m/^[0-9a-f]+\.0/);
        # get certificate subject
        my $X509 = Crypt::OpenSSL::X509->new_from_file($capath.'/'.$file);
        my $ca_dn = $X509->subject();
        $ca_dn =~ s/(^|,\s)/\//g;
        # add subject to CA list
        $ref_ca_list->{$ca_dn} = $ca_dn;
        $ca_list_string .= "    $ca_dn\n"
    }
    &Logger("SUPPORTED Certificate Authorities:\n$ca_list_string", DEBUG);
    closedir(CADIR);
}

# check issuer dn is in the list of allowed
sub issuer_match {
    my ($dn, $issuer, $ref_ca_list) = @_;
    $issuer=~s/\s+$//;

    unless ( $ref_ca_list->{$issuer} ) {
        &Logger("User '$dn' is denied (certificates issued by $issuer are NOT Authenticated)", DEBUG);
        return 0;
    }

    return 1;
}

#
# CACHE OPERATIONS SUBROUTINES
#

# get source URL hash
sub urlhash {
    my $url = shift;
    # split the url into substrings of length 8 and run crypt on each substring
    my @chunks = ( $url =~ /.{1,8}/gs );
    my $result;
    foreach my $c (@chunks) {
        $result .= crypt $c, "arc";
    }
    $result =~ s/[\/|\.]//g;
    return $result;
}

# get cache location for source URL
sub get_subject_cache_location {
    my $url = shift;
    my $hash = &urlhash($url);
    my $file_location = $cachedir . "/" . $hash; 
    return $file_location;
}

# write cached values for source URL
sub write_cached_subjects {
    my ($url, $ref_subjects) = @_;
    my %Subjects = %$ref_subjects;

    my $cache_file = &get_subject_cache_location($url);

    &Logger("Writting cached subjects for $url to $cache_file", DEBUG);
    store($ref_subjects, $cache_file) or &Logger("Failed to write to the cache file $cache_file", WARNING);
}

# read cached values for source URL
sub read_cached_subjects {
    my $url = shift;

    my $cache_file = &get_subject_cache_location($url);

    unless ( -e $cache_file ) {
        &Logger("Cache file does not exists for URL: $url", VERBOSE);
        return 1;
    }

    my $mtime = (stat($cache_file))[9];
    if ($mtime + $cache_maxlife < time()) {
        &Logger("Rejecting to use cache, max lifetime exceeded", VERBOSE);
        eval { unlink($cache_file); };
        return 2;
    }

    &Logger("Getting subjects for $url from cache", DEBUG);
    my $ref_subjects;
    eval { $ref_subjects = retrieve($cache_file); };
    if ( defined $ref_subjects ) {
        return 0, $ref_subjects;
    }

    &Logger("Failed to get data from cache file for URL: $url", WARNING);
    eval { unlink($cache_file); };
    return 3;
}

#
# LOGGING FUNCTIONS
#

# convert debug level to number
sub debug_numericv {
    my $level = shift;
    return $level if ( $level =~ /\d/ );
    return 0 if $level =~ /^FATAL$/i;
    return 1 if $level =~ /^ERROR$/i;
    return 2 if $level =~ /^WARNING$/i;
    return 3 if $level =~ /^INFO$/i;
    return 4 if $level =~ /^VERBOSE$/i;
    return 5 if $level =~ /^DEBUG$/i;
    return 2; # WARNING level on syntax error
}

# get debug level string value
sub debug_stringv {
    my $level = shift;
    return "FATAL" if ( $level == 0 );
    return "ERROR" if ( $level == 1 );
    return "WARNING" if ( $level == 2 );
    return "INFO" if ( $level == 3 );
    return "VERBOSE" if ( $level == 4 );
    return "DEBUG" if ( $level == 5 );
}

# show message depending on threshold
sub Logger {
    my ( $text, $threshold ) = @_;
    $threshold = &debug_numericv($threshold);
    if ( $threshold <= $debug_level ) {
        printf STDERR "%-7s: %s\n", &debug_stringv($threshold), $text;
    }
    # exit nordugridmap on FATAL errors
    exit (1) unless ( $threshold );
}

#
# DISPLAY NORDUGRIDMAP HELP
#

sub printHelp {
    system("pod2text $0");
}

=pod

=head1 NAME

nordugridmap - generates grid-mapfile(s)

=head1 SYNOPSIS

B<nordugridmap> [B<-t>, B<--test>] [B<-h>, B<--help>] [ B<-c>, B<--config> FILE ]

=head1 DESCRIPTION


B<nordugridmap> is usually run as a crontab entry
in order to automatically generate mapfile(s).
For configuration information consult tne Nordugruid ARC 
documentation and the arc.conf.template

=head1 OPTIONS

=over 4

=item B<-t>, B<--test>

Does not actually create grid-mapfile(s), but perform test
run in debug mode. 

=item B<-h>, B<--help>

Print a help screen.

=item B<-c>, B<--config> FILE

Specifies the configuration file to be used. By default the /etc/arc.conf is used. B<nordugridmap>
utilize [nordugridmap] section for general options fine-tunung and processes all the [vo] blocks from the config.

=back

=head1 CREDITS

The early scripts were based on a modified version of the mkgridmap (v 1.6) script
written by the DataGrid - authorization team <sec-grid@infn.it>. Since then the script
has been considerably rewritten.

In Dec 2011 script logic was completely rewriten and B<nordugridmap> v 2.0 was born.

=head1 COMMENTS

balazs.konya@hep.lu.se, waananen@nbi.dk, manf@grid.org.ua

=cut
