#!/usr/bin/perl
sub
toEntityIDkey {
my
(
$prefix
,
$entityID
) =
@_
;
my
$entityIDKey
=
$entityID
;
$entityIDKey
=~ s/^https?:\/\///;
$entityIDKey
=~ s/[^a-zA-Z0-9]/-/g;
$entityIDKey
=~ s/-+$//g;
return
(
$prefix
.
$entityIDKey
);
}
sub
get_config {
my
(
$array
) =
@_
;
my
%opts
;
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
) {
$config
->{
$section
} = { %{
$iniconfig
{
$section
} } };
}
}
for
my
$option
(
qw/verbose spconfprefix remove dry-run metadata idpconfprefix nagios/
)
{
if
(
defined
$opts
{
$option
} ) {
$config
->{main}->{
$option
} =
$opts
{
$option
};
}
}
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
;
}
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'
,
};
my
$spOptions
= {
'samlSPMetaDataOptionsCheckSLOMessageSignature'
=> 1,
'samlSPMetaDataOptionsCheckSSOMessageSignature'
=> 1,
'samlSPMetaDataOptionsEnableIDPInitiatedURL'
=> 0,
'samlSPMetaDataOptionsEncryptionMode'
=>
'none'
,
'samlSPMetaDataOptionsForceUTF8'
=> 1,
'samlSPMetaDataOptionsNameIDFormat'
=>
''
,
'samlSPMetaDataOptionsNotOnOrAfterTimeout'
=> 72000,
'samlSPMetaDataOptionsOneTimeUse'
=> 0,
'samlSPMetaDataOptionsSessionNotOnOrAfterTimeout'
=> 72000,
'samlSPMetaDataOptionsSignSLOMessage'
=> 1,
'samlSPMetaDataOptionsSignSSOMessage'
=> 1
};
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
};
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
},
};
$lastConf
->{samlSPMetaDataXML}->{
$confKey
}->{samlSPMetaDataXML} =
$partner_metadata
;
my
$requested_attributes_lmconf
=
_compute_requested_attributes(
$config
,
$entityID
,
$requestedAttributes
);
$lastConf
->{samlSPMetaDataExportedAttributes}->{
$confKey
} =
$requested_attributes_lmconf
;
$lastConf
->{samlSPMetaDataOptions}->{
$confKey
} =
_compute_sp_options(
$config
,
$entityID
);
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
},
};
$lastConf
->{samlIDPMetaDataXML}->{
$confKey
}->{samlIDPMetaDataXML} =
$partner_metadata
;
$lastConf
->{samlIDPMetaDataExportedAttributes}->{
$confKey
} =
_compute_idp_exported_attributes(
$config
,
$entityID
);
$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
;
if
(
$config
->{exportedAttributes} ) {
$result
= { %{
$config
->{exportedAttributes} } };
}
else
{
$result
= {
%$exportedAttributes
};
}
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-"
;
my
@spIgnorelist
= @{
$config
->{main}->{
'ignore-sp'
} || [] };
my
@idpIgnorelist
= @{
$config
->{main}->{
'ignore-idp'
} || [] };
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,
};
my
(
$allIdpList
,
$allSpList
,
$mdIdpList
,
$mdSpList
,
$matchingIdpList
,
$matchingSpList
);
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
);
foreach
my
$partner
(
$dom
->findnodes(
'/md:EntitiesDescriptor/md:EntityDescriptor'
) )
{
my
$entityID
=
$partner
->getAttribute(
'entityID'
);
$partner
->setNamespace(
"urn:oasis:names:tc:SAML:2.0:metadata"
,
"md"
, 0 );
$partner
->setNamespace(
"urn:oasis:names:tc:SAML:metadata:attribute"
,
"mdattr"
, 0 );
$partner
->setNamespace(
"urn:oasis:names:tc:SAML:2.0:assertion"
,
"saml"
, 0 );
my
$requested_subject_id
=
"none"
;
if
(
my
$subjectid
=
$partner
->findnodes(
'./md:Extensions'
.
'/mdattr:EntityAttributes'
.
'/saml:Attribute[@Name="urn:oasis:names:tc:SAML:profiles:subject-id:req"]'
.
'/saml:AttributeValue[1]'
.
'/text()'
)->
shift
()
)
{
$requested_subject_id
=
$subjectid
->toString;
}
foreach
(
$partner
->findnodes(
'.//md:Extensions'
) ) {
$_
->unbindNode; }
if
(
my
$idp
=
$partner
->findnodes(
'./md:IDPSSODescriptor'
) ) {
$idpCounter
->{found}++;
$mdIdpList
->{
$entityID
} = 1;
if
(
$partner
->findnodes(
'./md:IDPSSODescriptor/md:SingleSignOnService[contains(@Binding,"urn:oasis:names:tc:SAML:2.0:")]'
)
)
{
my
$partner_metadata
=
$partner
->toString;
$partner_metadata
=~ s/\n//g;
utf8::encode(
$partner_metadata
);
if
(
grep
{
$entityID
eq
$_
}
@idpIgnorelist
) {
printlog(
"IDP $entityID won't be update/added \n"
);
$idpCounter
->{ignored}++;
}
else
{
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
} ) {
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;
if
(
$partner
->findnodes(
'./md:SPSSODescriptor/md:AssertionConsumerService[contains(@Binding,"urn:oasis:names:tc:SAML:2.0:")]'
)
)
{
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;
if
(
$friendlyname
) {
$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'
},
};
}
if
(
$requested_subject_id
eq
"any"
or
$requested_subject_id
eq
"subject-id"
)
{
$requestedAttributes
->{
"subjectId"
} = {
required
=> 0,
name
=>
"urn:oasis:names:tc:SAML:attribute:subject-id"
,
name_format
=>
"urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
,
friendly_name
=>
"subject-id"
,
};
}
foreach
(
$partner
->findnodes(
'./md:SPSSODescriptor/md:AttributeConsumingService'
)
)
{
$_
->unbindNode;
}
my
$partner_metadata
=
$partner
->toString;
$partner_metadata
=~ s/\n//g;
utf8::encode(
$partner_metadata
);
if
(
grep
{
$entityID
eq
$_
}
@spIgnorelist
) {
printlog(
"SP $entityID won't be update/added \n"
);
$spCounter
->{ignored}++;
}
else
{
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
} ) {
$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}++;
}
}
}
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
);
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"
);
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'
} ) {
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
;
}
unless
(
caller
) {
main();
}