888 lines
29 KiB
Perl
Executable File
888 lines
29 KiB
Perl
Executable File
#!/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,
|
|
'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>
|