#!/usr/bin/perl

use strict;
use Getopt::Long qw/GetOptionsFromArray/;
use Pod::Usage;
use Lemonldap::NG::Common::Conf;
use LWP::UserAgent;
use MIME::Base64;
use Config::IniFiles;
use XML::LibXML;

sub toEntityIDkey {
    my ( $prefix, $entityID ) = @_;

    my $entityIDKey = $entityID;
    $entityIDKey =~ s/^https?:\/\///;
    $entityIDKey =~ s/[^a-zA-Z0-9]/-/g;
    $entityIDKey =~ s/-+$//g;
    return ( $prefix . $entityIDKey );
}

# Get configuration from CLI opts and config file
# CLI arguments take priority
# Multi value arguments are extended instead of replaced
sub get_config {
    my ($array) = @_;

    my %opts;

    # Get command line options
    my $result = GetOptionsFromArray(
        $array,              \%opts,
        'metadata|m=s',      'verbose|v',
        'help|h',            'spconfprefix|s=s',
        'idpconfprefix|i=s', 'remove|r',
        'nagios|a',          'ignore-sp=s@',
        'ignore-idp=s@',     'dry-run|n',
        'configfile|c=s',
    );

    pod2usage(1) if $opts{help};

    my $config;
    if ( my $configfile = $opts{configfile} ) {
        my %iniconfig;
        tie %iniconfig, 'Config::IniFiles',
          ( -file => $configfile, -allowempty => 1 );
        for my $section ( keys %iniconfig ) {

            # Copy tied hash into normal hash
            $config->{$section} = { %{ $iniconfig{$section} } };
        }

    }

    # Handle scalar options
    for my $option (
        qw/verbose spconfprefix remove dry-run metadata idpconfprefix nagios/)
    {
        if ( defined $opts{$option} ) {
            $config->{main}->{$option} = $opts{$option};
        }
    }

    # Handle arrayref options by appending values from file to values from CLI
    for my $option (qw/ignore-sp ignore-idp/) {
        my $value_from_cmdline = $opts{$option} || [];
        my $value_from_conf    = $config->{main}->{$option};
        if ( ref($value_from_conf) ne "ARRAY" ) {
            if ($value_from_conf) {
                $config->{main}->{$option} = [$value_from_conf];
            }
            else {
                $config->{main}->{$option} = [];
            }
        }
        if ( defined $value_from_cmdline ) {
            push @{ $config->{main}->{$option} }, @$value_from_cmdline;
        }
    }

    return $config;
}

#==============================================================================
# Default values
#==============================================================================

# Set here attributes that are declared for your SP in the federation
# They will be set as exported attributes for all IDP
#
my $exportedAttributes = {
    'cn'                          => '0;cn',
    'eduPersonPrincipalName'      => '0;eduPersonPrincipalName',
    'givenName'                   => '0;givenName',
    'surname'                     => '0;surname',
    'displayName'                 => '0;displayName',
    'eduPersonAffiliation'        => '0;eduPersonAffiliation',
    'eduPersonPrimaryAffiliation' => '0;eduPersonPrimaryAffiliation',
    'mail'                        => '0;mail',
    'supannListeRouge'            => '0;supannListeRouge',
    'supannEtuCursusAnnee'        => '0;supannEtuCursusAnnee',
};

# Set here options that are applied on all SP from the federation
my $spOptions = {
    'samlSPMetaDataOptionsCheckSLOMessageSignature'   => 1,
    'samlSPMetaDataOptionsCheckSSOMessageSignature'   => 1,
    'samlSPMetaDataOptionsEnableIDPInitiatedURL'      => 0,
    'samlSPMetaDataOptionsEncryptionMode'             => 'none',
    'samlSPMetaDataOptionsForceUTF8'                  => 1,
    'samlSPMetaDataOptionsNameIDFormat'               => '',
    'samlSPMetaDataOptionsNotOnOrAfterTimeout'        => 72000,
    'samlSPMetaDataOptionsOneTimeUse'                 => 0,
    'samlSPMetaDataOptionsSessionNotOnOrAfterTimeout' => 72000,
    'samlSPMetaDataOptionsSignSLOMessage'             => 1,
    'samlSPMetaDataOptionsSignSSOMessage'             => 1
};

# Set here options that are applied on all IDP from the federation
my $idpOptions = {
    'samlIDPMetaDataOptionsAdaptSessionUtime'        => 0,
    'samlIDPMetaDataOptionsAllowLoginFromIDP'        => 0,
    'samlIDPMetaDataOptionsAllowProxiedAuthn'        => 0,
    'samlIDPMetaDataOptionsCheckAudience'            => 1,
    'samlIDPMetaDataOptionsCheckSLOMessageSignature' => 1,
    'samlIDPMetaDataOptionsCheckSSOMessageSignature' => 1,
    'samlIDPMetaDataOptionsCheckTime'                => 1,
    'samlIDPMetaDataOptionsEncryptionMode'           => 'none',
    'samlIDPMetaDataOptionsForceAuthn'               => 0,
    'samlIDPMetaDataOptionsForceUTF8'                => 0,
    'samlIDPMetaDataOptionsIsPassive'                => 0,
    'samlIDPMetaDataOptionsNameIDFormat'             => 'transient',
    'samlIDPMetaDataOptionsRelayStateURL'            => 0,
    'samlIDPMetaDataOptionsSignSLOMessage'           => -1,
    'samlIDPMetaDataOptionsSignSSOMessage'           => -1,
    'samlIDPMetaDataOptionsStoreSAMLToken'           => 0
};

#==============================================================================
# Main
#==============================================================================

our $verbose = 0;

sub printlog {
    if ($verbose) {
        print @_;
    }
}

sub printlogerr {
    if ($verbose) {
        print STDERR @_;
    }
}

sub register_saml_sp {
    my ( $config, $lastConf, $confKey, $entityID, $partner_metadata,
        $requestedAttributes )
      = @_;

    my $old_provider = {
        xml => $lastConf->{samlSPMetaDataXML}->{$confKey}->{samlSPMetaDataXML},
        attributes => $lastConf->{samlSPMetaDataExportedAttributes}->{$confKey},
        options    => $lastConf->{samlSPMetaDataOptions}->{$confKey},
    };

    # Update metadata
    $lastConf->{samlSPMetaDataXML}->{$confKey}->{samlSPMetaDataXML} =
      $partner_metadata;

    my $requested_attributes_lmconf =
      _compute_requested_attributes( $config, $entityID, $requestedAttributes );

    # Update attributes
    $lastConf->{samlSPMetaDataExportedAttributes}->{$confKey} =
      $requested_attributes_lmconf;

    $lastConf->{samlSPMetaDataOptions}->{$confKey} =
      _compute_sp_options( $config, $entityID );

    # handle eduPersonTargetedID
    if ( $lastConf->{samlSPMetaDataExportedAttributes}->{$confKey}
        ->{eduPersonTargetedID} )
    {
        delete $lastConf->{samlSPMetaDataExportedAttributes}->{$confKey}
          ->{eduPersonTargetedID};
        $lastConf->{samlSPMetaDataOptions}->{$confKey}
          ->{samlSPMetaDataOptionsNameIDFormat} = 'persistent';
    }

    return _provider_equal( $lastConf, $old_provider, $confKey, "SP" );
}

sub _compute_idp_options {
    my ( $config, $entityID ) = @_;
    return _compute_options( $config, $entityID, "samlIDPMetaDataOptions",
        $idpOptions );
}

sub _compute_sp_options {
    my ( $config, $entityID ) = @_;
    return _compute_options( $config, $entityID, "samlSPMetaDataOptions",
        $spOptions );
}

sub _compute_options {
    my ( $config, $entityID, $option_prefix, $defaultOptions ) = @_;

    my $provider_options        = {%$defaultOptions};
    my $global_provider_options = $config->{"ALL"} || {};
    for my $key ( grep /^$option_prefix/, keys %$global_provider_options ) {
        $provider_options->{$key} = $global_provider_options->{$key};
    }
    my $entityid_provider_options = $config->{$entityID} || {};
    for my $key ( grep /^$option_prefix/, keys %$entityid_provider_options ) {
        $provider_options->{$key} = $entityid_provider_options->{$key};
    }
    return $provider_options;
}

sub _compute_requested_attributes {
    my ( $config, $entityID, $requestedAttributes ) = @_;
    my $result = {};

    for my $name ( keys %$requestedAttributes ) {
        my $attrconf = $requestedAttributes->{$name};
        my $is_required =
          $config->{"$entityID"}->{"attribute_required_$name"}
          // $config->{"$entityID"}->{"attribute_required"}
          // $config->{"ALL"}->{"attribute_required_$name"}
          // $config->{"ALL"}->{"attribute_required"} // $attrconf->{required};
        my $req_attr_str = join( ';',
            ( $is_required // 0 ),
            $attrconf->{name},
            ( $attrconf->{name_format}   || '' ),
            ( $attrconf->{friendly_name} || '' ),
        );
        $result->{$name} = $req_attr_str;
    }
    return $result;
}

sub register_saml_idp {
    my ( $config, $lastConf, $confKey, $entityID, $partner_metadata ) = @_;

    my $old_provider = {
        xml =>
          $lastConf->{samlIDPMetaDataXML}->{$confKey}->{samlIDPMetaDataXML},
        attributes =>
          $lastConf->{samlIDPMetaDataExportedAttributes}->{$confKey},
        options => $lastConf->{samlIDPMetaDataOptions}->{$confKey},
    };

    # Set Metadata
    $lastConf->{samlIDPMetaDataXML}->{$confKey}->{samlIDPMetaDataXML} =
      $partner_metadata;

    # Set attributes
    $lastConf->{samlIDPMetaDataExportedAttributes}->{$confKey} =
      _compute_idp_exported_attributes( $config, $entityID );

    # Set options
    $lastConf->{samlIDPMetaDataOptions}->{$confKey} =
      _compute_idp_options( $config, $entityID );

    return _provider_equal( $lastConf, $old_provider, $confKey, "IDP" );
}

sub _compute_idp_exported_attributes {
    my ( $config, $entityID ) = @_;
    my $result;

    # Use default exported attributes from config or from hardcoded list
    if ( $config->{exportedAttributes} ) {
        $result = { %{ $config->{exportedAttributes} } };
    }
    else {
        $result = {%$exportedAttributes};
    }

    # Override according to configuration
    my $idp_config = $config->{"$entityID"} || {};
    for my $key ( grep /^exported_attribute_/, keys(%$idp_config) ) {
        my ($attribute_name) = $key =~ m/exported_attribute_(.*)$/;
        $result->{$attribute_name} = $idp_config->{$key};
    }
    return $result;
}

sub _provider_equal {
    my ( $lastConf, $old_provider, $confKey, $type ) = @_;

    my $xml_equal =
      $old_provider->{xml} eq $lastConf->{"saml${type}MetaDataXML"}->{$confKey}
      ->{"saml${type}MetaDataXML"};
    my $attributes_equal = _hash_equal( $old_provider->{attributes},
        $lastConf->{"saml${type}MetaDataExportedAttributes"}->{$confKey} );
    my $options_equal = _hash_equal( $old_provider->{options},
        $lastConf->{"saml${type}MetaDataOptions"}->{$confKey} );

    return ( $xml_equal and $attributes_equal and $options_equal );
}

sub _hash_equal {
    my ( $hash1, $hash2 ) = @_;
    return 0 unless $hash1;
    return 0 unless $hash2;
    return 0 unless ( %$hash1 == %$hash2 );
    for my $key ( keys %$hash1 ) {
        return 0 unless ( $hash1->{$key} || '' ) eq ( $hash2->{$key} || '' );
    }
    return 1;
}

sub transform_config {
    my ( $config, $lastConf, $xml_metadata ) = @_;

    my $spConfKeyPrefix  = $config->{main}->{spconfprefix}  || "sp-";
    my $idpConfKeyPrefix = $config->{main}->{idpconfprefix} || "idp-";

    # BlockList initialisation
    my @spIgnorelist  = @{ $config->{main}->{'ignore-sp'}  || [] };
    my @idpIgnorelist = @{ $config->{main}->{'ignore-idp'} || [] };

    # Stats initialization
    my $idpCounter = {
        'found'    => 0,
        'updated'  => 0,
        'created'  => 0,
        'rejected' => 0,
        'removed'  => 0,
        'ignored'  => 0
    };
    my $spCounter = {
        'found'    => 0,
        'updated'  => 0,
        'created'  => 0,
        'rejected' => 0,
        'removed'  => 0,
        'ignored'  => 0,
    };

    # IDP and SP lists
    my ( $allIdpList, $allSpList, $mdIdpList, $mdSpList, $matchingIdpList,
        $matchingSpList );

    # List current SAML partners
    foreach my $spConfKey ( keys %{ $lastConf->{samlSPMetaDataXML} } ) {
        my ( $tmp, $entityID ) =
          ( $lastConf->{samlSPMetaDataXML}->{$spConfKey}->{samlSPMetaDataXML}
              =~ /entityID=(['"])(.+?)\1/si );
        $allSpList->{$entityID} = $spConfKey;
        if ( $spConfKey =~ /^$spConfKeyPrefix/ ) {
            $matchingSpList->{$entityID} = $spConfKey;
        }
        printlog("Existing SAML partner found: [SP] $entityID ($spConfKey)\n");
    }

    foreach my $idpConfKey ( keys %{ $lastConf->{samlIDPMetaDataXML} } ) {
        my ( $tmp, $entityID ) =
          ( $lastConf->{samlIDPMetaDataXML}->{$idpConfKey}->{samlIDPMetaDataXML}
              =~ /entityID=(['"])(.+?)\1/si );
        $allIdpList->{$entityID} = $idpConfKey;
        if ( $idpConfKey =~ /^$idpConfKeyPrefix/ ) {
            $matchingIdpList->{$entityID} = $idpConfKey;
        }
        printlog(
            "Existing SAML partner found: [IDP] $entityID ($idpConfKey)\n");
    }

    my $dom = XML::LibXML->load_xml( string => $xml_metadata );

    # Remove extensions
    foreach ( $dom->findnodes('//md:Extensions') ) { $_->unbindNode; }

    # Browse all partners
    foreach my $partner (
        $dom->findnodes('/md:EntitiesDescriptor/md:EntityDescriptor') )
    {
        my $entityID = $partner->getAttribute('entityID');

        # Add required XML namespaces
        $partner->setNamespace( "urn:oasis:names:tc:SAML:2.0:metadata",
            "md", 0 );
        $partner->setNamespace( "urn:oasis:names:tc:SAML:2.0:assertion",
            "saml", 0 );
        $partner->setNamespace( "http://www.w3.org/2000/09/xmldsig#", "ds", 0 );

        # Check IDP or SP
        if ( my $idp = $partner->findnodes('./md:IDPSSODescriptor') ) {
            $idpCounter->{found}++;
            $mdIdpList->{$entityID} = 1;

            # Check if SAML 2.0 is supported
            if (
                $partner->findnodes(
'./md:IDPSSODescriptor/md:SingleSignOnService[contains(@Binding,"urn:oasis:names:tc:SAML:2.0:")]'
                )
              )
            {

                # Read metadata
                my $partner_metadata = $partner->toString;
                $partner_metadata =~ s/\n//g;

                # test if IDP entityID is inside the block list

                if ( grep { $entityID eq $_ } @idpIgnorelist ) {
                    printlog("IDP $entityID won't be update/added \n");
                    $idpCounter->{ignored}++;
                }
                else {
                    # Check if entityID already in configuration
                    if ( defined $matchingIdpList->{$entityID} ) {

                        my $confKey = $matchingIdpList->{$entityID};
                        my $equal =
                          register_saml_idp( $config, $lastConf, $confKey,
                            $entityID, $partner_metadata );

                        if ($equal) {
                            printlog("IDP $entityID has not changed\n");
                        }
                        else {
                            printlog("Update IDP $entityID in configuration\n");
                            $idpCounter->{updated}++;
                        }
                    }
                    elsif ( not defined $allIdpList->{$entityID} ) {

                        # Create a new partner
                        my $confKey =
                          toEntityIDkey( $idpConfKeyPrefix, $entityID );

                        register_saml_idp( $config, $lastConf, $confKey,
                            $entityID, $partner_metadata );

                        printlog( "Declare new IDP $entityID"
                              . " (configuration key $confKey)\n" );
                        $idpCounter->{created}++;
                    }
                    else {
                        my $confKey = $allIdpList->{$entityID};
                        printlog( "Skipping existing IDP $entityID"
                              . " (configuration key $confKey)\n" );
                        $idpCounter->{ignored}++;
                    }
                }

            }
            else {
                printlogerr(
                        "[WARN] IDP $entityID is not compatible with SAML 2.0,"
                      . " it will not be imported.\n" );
                $idpCounter->{rejected}++;
            }
        }
        if ( my $sp = $partner->findnodes('./md:SPSSODescriptor') ) {
            $spCounter->{found}++;
            $mdSpList->{$entityID} = 1;

            # Check if SAML 2.0 is supported
            if (
                $partner->findnodes(
'./md:SPSSODescriptor/md:AssertionConsumerService[contains(@Binding,"urn:oasis:names:tc:SAML:2.0:")]'
                )
              )
            {

                # Read requested attributes
                my $requestedAttributes = {};
                if (
                    $partner->findnodes(
'./md:SPSSODescriptor/md:AttributeConsumingService/md:RequestedAttribute'
                    )
                  )
                {
                    foreach my $requestedAttribute (
                        $partner->findnodes(
'./md:SPSSODescriptor/md:AttributeConsumingService/md:RequestedAttribute'
                        )
                      )
                    {
                        my $name = $requestedAttribute->getAttribute("Name");
                        my $friendlyname =
                          $requestedAttribute->getAttribute("FriendlyName");
                        my $nameformat =
                          $requestedAttribute->getAttribute("NameFormat");
                        my $required =
                          ( $requestedAttribute->getAttribute("isRequired") =~
                              /true/i ) ? 1 : 0;
                        $requestedAttributes->{$friendlyname} = {
                            required      => $required,
                            name          => $name,
                            name_format   => $nameformat,
                            friendly_name => $friendlyname
                        };
                        printlog( "Attribute $friendlyname ($name)"
                              . " requested by SP $entityID\n" );
                    }
                }
                else {
                    $requestedAttributes = {
                        cn   => { required => 1, name => 'cn' },
                        uid  => { required => 1, name => 'uid' },
                        mail => { required => 1, name => 'mail' },
                    };
                }

                # Remove AttributeConsumingService node
                foreach (
                    $partner->findnodes(
                        './md:SPSSODescriptor/md:AttributeConsumingService')
                  )
                {
                    $_->unbindNode;
                }

                # Read metadata
                my $partner_metadata = $partner->toString;
                $partner_metadata =~ s/\n//g;

                # test if IDP entityID is inside the block list

                if ( grep { $entityID eq $_ } @spIgnorelist ) {
                    printlog("SP $entityID won't be update/added \n");
                    $spCounter->{ignored}++;
                }
                else {
                    # Check if entityID already in configuration
                    my $confKey;
                    if ( defined $matchingSpList->{$entityID} ) {
                        $confKey = $matchingSpList->{$entityID};

                        my $equal =
                          register_saml_sp( $config, $lastConf, $confKey,
                            $entityID,
                            $partner_metadata, $requestedAttributes );

                        if ($equal) {
                            printlog("SP $entityID has not changed\n");
                        }
                        else {
                            printlog("Update SP $entityID in configuration\n");
                            $spCounter->{updated}++;
                        }
                    }
                    elsif ( not defined $allSpList->{$entityID} ) {

                        # Create a new partner
                        $confKey = toEntityIDkey( $spConfKeyPrefix, $entityID );

                        register_saml_sp( $config, $lastConf, $confKey,
                            $entityID,
                            $partner_metadata, $requestedAttributes );

                        printlog( "Declare new SP $entityID"
                              . " (configuration key $confKey)\n" );
                        $spCounter->{created}++;
                    }
                    else {
                        my $entityID = $allSpList->{$entityID};
                        printlog( "Skipping existing SP $entityID "
                              . "(configuration key $confKey)\n" );
                        $spCounter->{ignored}++;
                    }

                }

            }
            else {
                printlogerr(
                        "[WARN] SP $entityID is not compatible with SAML 2.0,"
                      . " it will not be imported.\n" );
                $spCounter->{rejected}++;
            }

        }

    }

    # Remove partners
    if ( $config->{main}->{remove} ) {
        foreach my $entityID ( keys %$matchingIdpList ) {
            my $idpConfKey = $matchingIdpList->{$entityID};
            unless ( defined $mdIdpList->{$entityID} ) {
                if ( grep { $entityID eq $_ } @idpIgnorelist ) {
                    $idpCounter->{ignored}++;
                    printlog("IDP $idpConfKey won't be deleted \n");
                }
                else {
                    delete $lastConf->{samlIDPMetaDataXML}->{$idpConfKey};
                    delete $lastConf->{samlIDPMetaDataExportedAttributes}
                      ->{$idpConfKey};
                    delete $lastConf->{samlIDPMetaDataOptions}->{$idpConfKey};
                    $idpCounter->{removed}++;
                    printlog("Remove IDP $idpConfKey\n");
                }
            }
        }

        foreach my $entityID ( keys %$matchingSpList ) {
            my $spConfKey = $matchingSpList->{$entityID};
            unless ( defined $mdSpList->{$entityID} ) {
                if ( grep { $entityID eq $_ } @spIgnorelist ) {
                    $spCounter->{ignored}++;
                    printlog("SP $spConfKey won't be deleted \n");
                }
                else {
                    delete $lastConf->{samlSPMetaDataXML}->{$spConfKey};
                    delete $lastConf->{samlSPMetaDataExportedAttributes}
                      ->{$spConfKey};
                    delete $lastConf->{samlSPMetaDataOptions}->{$spConfKey};
                    $spCounter->{removed}++;
                    printlog("Remove SP $spConfKey\n");
                }
            }
        }
    }
    return ( $spCounter, $idpCounter );
}

sub main {

    my $config = get_config( \@ARGV );

    # Get merged config
    my %opts = %{ $config->{main} };
    $verbose = $opts{verbose};

    pod2usage( -message => "Missing metadata URL (-m)", -exitval => 2 )
      if !$opts{metadata};

    my $conf     = Lemonldap::NG::Common::Conf->new();
    my $lastConf = $conf->getConf();

    printlog( "Read configuration " . $lastConf->{cfgNum} . "\n" );

    # Download metadata file
    my $ua = LWP::UserAgent->new;
    $ua->timeout(10);
    $ua->env_proxy;

    my $metadata_file = $opts{metadata};

    printlog("Try to download metadata file at $metadata_file\n");
    my $response = $ua->get($metadata_file);

    if ( $response->is_success ) {
        printlog("Metadata file found\n");
    }
    else {
        die $response->status_line;
    }

    my $xml_metadata = $response->decoded_content;

    my ( $spCounter, $idpCounter ) =
      transform_config( $config, $lastConf, $xml_metadata );

    my $numConf  = "DRY-RUN";
    my $exitCode = 0;

    if ( !$opts{'dry-run'} ) {

        # Register configuration
        printlog("[INFO] run mod EntityID will be inserted\n");
        $numConf = $conf->saveConf( $lastConf, ( cfgNumFixed => 1 ) );
        printlog("[OK] Configuration $numConf saved\n");
        unless ($numConf > 0) {
            print "[ERROR] Unable to save configuration\n";
            $exitCode = 1;
        }
    }
    else {
        printlog("[INFO] Dry-run mod no EntityID inserted\n");
    }

    if ( $opts{nagios} ) {
        print "Metadata loaded inside Conf: ["
          . $numConf
          . "]|idp_found="
          . $idpCounter->{found}
          . ", idp_updated="
          . $idpCounter->{updated}
          . ", idp_created="
          . $idpCounter->{created}
          . ", idp_removed="
          . $idpCounter->{removed}
          . ", idp_rejected="
          . $idpCounter->{rejected}
          . ", idp_ignored="
          . $idpCounter->{ignored}
          . ", sp_found="
          . $spCounter->{found}
          . ", sp_updated="
          . $spCounter->{updated}
          . ", sp_created="
          . $spCounter->{created}
          . ", sp_removed="
          . $spCounter->{removed}
          . ", sp_rejected="
          . $spCounter->{rejected}
          . ", sp_ignored="
          . $spCounter->{ignored} . "\n";
    }
    else {
        print "[IDP]\tFound: "
          . $idpCounter->{found}
          . "\tUpdated: "
          . $idpCounter->{updated}
          . "\tCreated: "
          . $idpCounter->{created}
          . "\tRemoved: "
          . $idpCounter->{removed}
          . "\tRejected: "
          . $idpCounter->{rejected}
          . "\tIgnored: "
          . $idpCounter->{ignored} . "\n";
        print "[SP]\tFound: "
          . $spCounter->{found}
          . "\tUpdated: "
          . $spCounter->{updated}
          . "\tCreated: "
          . $spCounter->{created}
          . "\tRemoved: "
          . $spCounter->{removed}
          . "\tRejected: "
          . $spCounter->{rejected}
          . "\tIgnored: "
          . $spCounter->{ignored} . "\n";
    }

    exit $exitCode;
}

# If run as a script
unless (caller) {
    main();
}

__END__
Script to import SAML metadata bundle file into LL::NG configuration\n\n";
Usage: $0 -m <metadata file URL>\n\n";
Options:\n";

=encoding UTF-8

=head1 NAME

importMetadata - Script to import SAML federation metadata into LL::NG configuration

=head1 SYNOPSIS

importMetadata -m <metadata URL> [options]

Options:

    -m, --metadata          URL of metadata document
    -i, --idpconfprefix     Prefix used to set IDP configuration key
    -s, --spconfprefix      Prefix used to set SP configuration key
    --ignore-sp             ignore SP matching this entityID (can be specified multiple times)
    --ignore-idp            ignore IdP matching this entityID (can be specified multiple times)
    -a, --nagios            output statistics in Nagios format
    -r, --remove            remove provider from LemonLDAP::NG if it does not appear in metadata
    -n, --dry-run           print statistics but do not apply changes
    -c, --config-file       use provided configuration file
    -v, --verbose           increase verbosity of output
    -h, --help              print full documentation

=head1 OPTIONS

=over

=item B<-m I<URL>>, B<--metadata=I<URL>>

Specifies the <URL> of the metadata document to import

=item B<-i I<PREFIX>>, B<--idpconfprefix=I<PREFIX>>

Prefix each IDP found the metadata document with the <PREFIX> when registring
them into LemonLDAP::NG

=item B<-s I<PREFIX>>, B<--spconfprefix=I<PREFIX>>

Prefix each SP found the metadata document with the <PREFIX> when registring
them into LemonLDAP::NG

=item B<--ignore-sp=I<ENTITYID>>

Ignore the specified Service Provider <ENTITYID>. It will not be added, updated
or deleted from LemonLDAP::NG configuration

=item B<--ignore-idp=I<ENTITYID>>

Ignore the specified Identity Provider <ENTITYID>. It will not be added,
updated or deleted from LemonLDAP::NG configuration

=item B<-a>, B<--nagios>

After each run, print statistics about added/modified/deleted items in Nagios
format

=item B<-r>, B<--remove>

If this option is used, after a successful import, existing SP/IDPs who match
the configuration prefix will be removed from LemonLDAP::NG if they were not
present in the imported metadata

=item B<-n>, B<--dry-run>

This option prevents the modified configuration from being saved. It can be used for testing.

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

Using a configuration file lets you do advanced configuration on a global per-provider basis.
The configuration file is stored in .ini format. Here is an example file

    # main script options, these will be overriden by the CLI options
    [main]
    dry-run=1
    verbose=1
    metadata=http://url/to/metadata.xml
    ; Multi-value options
    ignore-idp=entity-id-to-ignore-1
    ignore-idp=entity-id-to-ignore-2

    # Default exported attributes for IDPs
    [exportedAttributes]
    cn=0;cn
    eduPersonPrincipalName=0;eduPersonPrincipalName
    ...

    # options that apply to all providers
    [ALL]
    ; Disable signature requirement on requests
    samlSPMetaDataOptionsCheckSSOMessageSignature=0
    samlSPMetaDataOptionsCheckSLOMessageSignature=0
    ; Store SAML assertions in session
    samlIDPMetaDataOptionsStoreSAMLToken=1
    ; Mark ePPN as always required
    attribute_required_eduPersonPrincipalName=1
    ...

    # Specific provider configurations
    [https://test-sp.federation.renater.fr]
    ; All attributes are optional for this provider
    attribute_required=0
    ; Override some options
    samlSPMetaDataOptionsNameIDFormat=persistent

    [https://idp.renater.fr/idp/shibboleth]
    ; declare an extra attribute from this provider
    exported_attribute_eduPersonAffiliation=1;uid

=item B<-v>, B<--verbose>

Increase verbosity during script execution

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

Displays the script's documentation

=back

=head1 SEE ALSO

L<http://lemonldap-ng.org/>

=head1 AUTHORS

=over

=item Clement Oudot, E<lt>clement@oodo.netE<gt>

=back

=head1 BUG REPORT

Use OW2 system to report bug or ask for features:
L<https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues>

=head1 DOWNLOAD

Lemonldap::NG is available at
L<https://lemonldap-ng.org/download>
