lemonldap-ng/lemonldap-ng-common/scripts/importMetadata

641 lines
20 KiB
Perl
Executable File

#!/usr/bin/perl
use strict;
use Getopt::Long;
use Pod::Usage;
use Lemonldap::NG::Common::Conf;
use LWP::UserAgent;
use MIME::Base64;
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 command line options
#==============================================================================
my %opts;
my $result = GetOptions(
\%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'
);
pod2usage(1) if $opts{help};
pod2usage( -message => "Missing metadata URL (-m)", -exitval => 2) if !$opts{metadata};
#==============================================================================
# Default values
#==============================================================================
my $spConfKeyPrefix = $opts{spconfprefix} || "sp-";
my $idpConfKeyPrefix = $opts{idpconfprefix} || "idp-";
# 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;eduPersonAffiliation',
'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
};
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,
};
# BlockList initialisation
my @spIgnorelist = @{ $opts{'ignore-sp'} || [] };
my @idpIgnorelist = @{ $opts{'ignore-idp'} || [] };
#==============================================================================
# Main
#==============================================================================
my $conf = Lemonldap::NG::Common::Conf->new();
my $lastConf = $conf->getConf();
if ( $opts{verbose} ) {
print "Read configuration " . $lastConf->{cfgNum} . "\n";
}
# IDP and SP lists
my ( $idpList, $spList, $mdIdpList, $mdSpList );
# List current SAML partners
foreach my $spConfKey ( keys %{ $lastConf->{samlSPMetaDataXML} } ) {
my ( $tmp, $entityID ) =
( $lastConf->{samlSPMetaDataXML}->{$spConfKey}->{samlSPMetaDataXML} =~
/entityID=(['"])(.+?)\1/si );
if ( $spConfKey =~ /^$spConfKeyPrefix/ ) {
$spList->{$entityID} = $spConfKey;
if ( $opts{verbose} ) {
print "Existing SAML partner found: [SP] $entityID ($spConfKey)\n";
}
}
}
foreach my $idpConfKey ( keys %{ $lastConf->{samlIDPMetaDataXML} } ) {
my ( $tmp, $entityID ) =
( $lastConf->{samlIDPMetaDataXML}->{$idpConfKey}->{samlIDPMetaDataXML} =~
/entityID=(['"])(.+?)\1/si );
if ( $idpConfKey =~ /^$idpConfKeyPrefix/ ) {
$idpList->{$entityID} = $idpConfKey;
if ( $opts{verbose} ) {
print
"Existing SAML partner found: [IDP] $entityID ($idpConfKey)\n";
}
}
}
# Download metadata file
my $ua = LWP::UserAgent->new;
$ua->timeout(10);
$ua->env_proxy;
my $metadata_file = $opts{metadata};
if ( $opts{verbose} ) {
print "Try to download metadata file at $metadata_file\n";
}
my $response = $ua->get($metadata_file);
if ( $response->is_success ) {
if ( $opts{verbose} ) {
print "Metadata file found\n";
}
}
else {
die $response->status_line;
}
my $dom = XML::LibXML->load_xml( string => $response->decoded_content );
# 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 ) {
if ( $opts{verbose} ) {
print "IDP $entityID won't be update/added \n";
}
$idpCounter->{ignored}++;
}
else {
# Check if entityID already in configuration
if ( defined $idpList->{$entityID} ) {
# Update metadata
$lastConf->{samlIDPMetaDataXML}->{ $idpList->{$entityID} }
->{samlIDPMetaDataXML} = $partner_metadata;
# Update attributes
$lastConf->{samlIDPMetaDataExportedAttributes}
->{ $idpList->{$entityID} } = $exportedAttributes;
# Update options
$lastConf->{samlIDPMetaDataOptions}
->{ $idpList->{$entityID} } = $idpOptions;
if ( $opts{verbose} ) {
print "Update IDP $entityID in configuration\n";
}
$idpCounter->{updated}++;
}
else {
# Create a new partner
my $confKey = toEntityIDkey( $idpConfKeyPrefix, $entityID );
# Metadata
$lastConf->{samlIDPMetaDataXML}->{$confKey}
->{samlIDPMetaDataXML} = $partner_metadata;
# Attributes
$lastConf->{samlIDPMetaDataExportedAttributes}->{$confKey}
= $exportedAttributes;
# Options
$lastConf->{samlIDPMetaDataOptions}->{$confKey} =
$idpOptions;
if ( $opts{verbose} ) {
print
"Declare new IDP $entityID (configuration key $confKey)\n";
}
$idpCounter->{created}++;
}
}
}
else {
print STDERR
"[WARN] IDP $entityID is not compatible with SAML 2.0, it will not be imported.\n"
if $opts{verbose};
$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;$name;$nameformat;$friendlyname";
if ( $opts{verbose} ) {
print
"Attribute $friendlyname ($name) requested by SP $entityID\n";
}
}
}
else {
$requestedAttributes =
{ 'cn' => '1;cn', 'uid' => '1;uid', 'mail' => '1;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 ) {
if ( $opts{verbose} ) {
print "SP $entityID won't be update/added \n";
}
$spCounter->{ignored}++;
}
else {
# Check if entityID already in configuration
if ( defined $spList->{$entityID} ) {
# Update metadata
$lastConf->{samlSPMetaDataXML}->{ $spList->{$entityID} }
->{samlSPMetaDataXML} = $partner_metadata;
# Update attributes
$lastConf->{samlSPMetaDataExportedAttributes}
->{ $spList->{$entityID} } = $requestedAttributes;
# Update options
# $lastConf->{samlSPMetaDataOptions}->{ $spList->{$entityID} } =
# $spOptions;
# FIX AGA
$lastConf->{samlSPMetaDataOptions}->{ $spList->{$entityID} }
= { %{$spOptions} };
if ( $opts{verbose} ) {
print "Update SP $entityID in configuration\n";
}
$spCounter->{updated}++;
}
else {
# Create a new partner
my $confKey = toEntityIDkey( $spConfKeyPrefix, $entityID );
# Metadata
$lastConf->{samlSPMetaDataXML}->{$confKey}
->{samlSPMetaDataXML} = $partner_metadata;
# Attributes
$lastConf->{samlSPMetaDataExportedAttributes}->{$confKey} =
$requestedAttributes;
# Options
# $lastConf->{samlSPMetaDataOptions}->{$confKey} = $spOptions;
# FIX AGA
$lastConf->{samlSPMetaDataOptions}->{$confKey} =
{ %{$spOptions} };
if ( $opts{verbose} ) {
print
"Declare new SP $entityID (configuration key $confKey)\n";
}
$spCounter->{created}++;
}
# handle eduPersonTargetedID
if ( $requestedAttributes->{eduPersonTargetedID} ) {
delete $requestedAttributes->{eduPersonTargetedID};
$lastConf->{samlSPMetaDataOptions}->{ $spList->{$entityID} }
->{samlSPMetaDataOptionsNameIDFormat} = 'persistent';
}
}
}
else {
print STDERR
"[WARN] SP $entityID is not compatible with SAML 2.0, it will not be imported.\n"
if $opts{verbose};
$spCounter->{rejected}++;
}
}
}
# Remove partners
if ( $opts{remove} ) {
foreach my $entityID ( keys %$idpList ) {
my $idpConfKey = $idpList->{$entityID};
unless ( defined $mdIdpList->{$entityID} ) {
if ( grep { $entityID eq $_ } @idpIgnorelist ) {
$idpCounter->{ignored}++;
if ( $opts{verbose} ) {
print "IDP $idpConfKey won't be deleted \n";
}
}
else {
delete $lastConf->{samlIDPMetaDataXML}->{$idpConfKey};
delete $lastConf->{samlIDPMetaDataExportedAttributes}
->{$idpConfKey};
delete $lastConf->{samlIDPMetaDataOptions}->{$idpConfKey};
$idpCounter->{removed}++;
if ( $opts{verbose} ) {
print "Remove IDP $idpConfKey\n";
}
}
}
}
foreach my $entityID ( keys %$spList ) {
my $spConfKey = $spList->{$entityID};
unless ( defined $mdSpList->{$entityID} ) {
if ( grep { $entityID eq $_ } @spIgnorelist ) {
$spCounter->{ignored}++;
if ( $opts{verbose} ) {
print "SP $spConfKey won't be deleted \n";
}
}
else {
delete $lastConf->{samlSPMetaDataXML}->{$spConfKey};
delete $lastConf->{samlSPMetaDataExportedAttributes}
->{$spConfKey};
delete $lastConf->{samlSPMetaDataOptions}->{$spConfKey};
$spCounter->{removed}++;
if ( $opts{verbose} ) {
print "Remove SP $spConfKey\n";
}
}
}
}
}
my $numConf = "DRY-RUN";
my $exitCode = 0;
if ( !$opts{'dry-run'} ) {
# Register configuration
if ( $opts{verbose} ) {
print "[INFO] run mod EntityID will be inserted\n";
}
$numConf = $conf->saveConf( $lastConf, ( cfgNumFixed => 1 ) );
if ( $opts{verbose} ) {
print "[OK] Configuration $numConf saved\n";
$exitCode = 0;
}
unless ($numConf) {
print "[ERROR] Unable to save configuration\n";
$exitCode = 1;
}
}
else {
if ( $opts{verbose} ) {
print "[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;
__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
-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<-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>