Skip to content
Snippets Groups Projects
Commit 5a851655 authored by renater.salaun's avatar renater.salaun
Browse files

Loading federation SAML metadata to build the drop-down menu for SP selection

git-svn-id: https://svn.geant.net/GEANT/edugain_testidp_account_manager/trunk@9 047e039d-479c-447e-8a29-aa6bf4a09bab
parent e53cfccb
No related branches found
No related tags found
No related merge requests found
......@@ -11,9 +11,10 @@ use Getopt::Long;
use POSIX;
use IdPAccountManager::TestAccount;
use IdPAccountManager::SAMLMetadata;
my %options;
unless (&GetOptions(\%options, 'help', 'create_test_account', 'account_profile=s', 'sp_entityid=s', 'list_test_accounts')) {
unless (&GetOptions(\%options, 'help', 'create_test_account', 'account_profile=s', 'sp_entityid=s', 'list_test_accounts', 'parse_federation_metadata')) {
die "Unknown options.";
}
......@@ -62,4 +63,24 @@ if ($options{'create_test_account'}) {
foreach my $test_account (@$all) {
$test_account->print();
}
}elsif ($options{'parse_federation_metadata'}) {
my $federation_metadata = new IdPAccountManager::SAMLMetadata;
unless ($federation_metadata->load(federation_metadata_file_path => $IdPAccountManager::Conf::global{'federation_metadata_file_path'})) {
die;
}
unless ($federation_metadata->parse()) {
die;
}
printf "Document %s parsed\n", $IdPAccountManager::Conf::global{'federation_metadata_file_path'};
## List SAML entities
printf "Hashref representing the metadata:\n";
&IdPAccountManager::Tools::dump_var($federation_metadata->{'federation_metadata_as_hashref'}, 0, \*STDOUT);
}else {
die "Missing arguments";
}
......@@ -2,9 +2,8 @@
## 15/09/2014, Olivier Salaün
## Web interface for the eduGAIN Test IdP Account Manager
## TODO : rename %erreurs e
use strict;
use strict vars;
use utf8;
use lib "/opt/testidp/IdPAccountManager/lib";
......@@ -16,6 +15,7 @@ use Template::Constants qw( :debug );
use POSIX;
use IdPAccountManager::TestAccount;
use IdPAccountManager::SAMLMetadata;
## Defining parameters format
my $urn_or_url_regex = '(http(s?):\/\/|urn:)[^\\\$\*\"\'\`\^\|\<\>\n\s]+'; ## Format de type URL HTTP ou URN
......@@ -27,7 +27,8 @@ my %format = (
#'attributeauthority' => $url_regex,
);
my %actions = ('select_sp' => {'title_en' => 'Select your Service Provider' }
my %actions = ('select_sp' => {'title_en' => 'Select your Service Provider' },
'get_sp_list' => {'title_en' => 'Select your Service Provider' },
);
## Gives writes for the group
......@@ -100,7 +101,7 @@ sub new {
}else {
## Default action
&IdPAccountManager::Tools::do_log('info', "Default action");
$request->{'action'} = 'help';
$request->{'action'} = 'get_sp_list';
}
bless $request, $pkg;
......@@ -121,7 +122,7 @@ sub execute {
defined $format{$key} &&
! ref($format{$key})) {
unless ($self->{'param_in'}{$key} =~ /^$format{$key}$/) {
push @{$self->{'param_out'}{'erreurs'}}, "format_$key";
push @{$self->{'param_out'}{'errors'}}, "format_$key";
&IdPAccountManager::Tools::do_log('error', "Incorrect parameter format : $key");
return undef;
}
......@@ -141,7 +142,7 @@ sub execute {
}else {
## Inknown action
push @{$self->{'param_out'}{'erreurs'}}, "unknown_action";
push @{$self->{'param_out'}{'errors'}}, "unknown_action";
&IdPAccountManager::Tools::do_log('error', "Unknown action '%s'", $self->{'action'});
}
......@@ -162,6 +163,9 @@ sub respond {
## Dump output data
#open TMP, ">/tmp/account_registry.out"; &IdPAccountManager::Tools::dump_var($self->{'param_out'}, 0, \*TMP); close TMP;
## Enable dumping off all variables in web pages
#$self->{'param_out'}{'dump'} = $self->{'param_out'};
## Automatic pass object entries to the output hash
foreach my $key (keys %{$self}) {
#&IdPAccountManager::Tools::do_log('trace', "Passing $key");
......@@ -195,17 +199,37 @@ sub respond {
}
## Ignore some type of errors
my @erreurs_admin;
foreach my $id_erreur (@{$self->{'param_out'}{'erreurs'}}) {
unless ($id_erreur =~ /^(error_x)$/) {
push @erreurs_admin, $id_erreur;
}
}
#my @errors_admin;
#foreach my $id_error (@{$self->{'param_out'}{'errors'}}) {
# unless ($id_error =~ /^(error_x)$/) {
# push @errors_admin, $id_error;
# }
#}
## Mail notification of admins about the error
if (@erreurs_admin) {
&IdPAccountManager::Tools::mail_notice('template' => 'templates/mail/notification_generic_error.tt2.eml',
&IdPAccountManager::Tools::mail_notice('template' => 'templates/mail/notification_generic_error.tt2.eml',
'data' => $self->{'param_out'});
}
}
\ No newline at end of file
}
## Return the list of known SPs
sub req_get_sp_list {
my $self = shift;
my $federation_metadata = new IdPAccountManager::SAMLMetadata;
unless ($federation_metadata->load(federation_metadata_file_path => $IdPAccountManager::Conf::global{'federation_metadata_file_path'})) {
push @{$self->{'param_out'}{'errors'}}, "internal";
&IdPAccountManager::Tools::do_log('error', "Failed to load federation metadata : $!");
return undef;
}
unless ($federation_metadata->parse()) {
push @{$self->{'param_out'}{'errors'}}, "internal";
&IdPAccountManager::Tools::do_log('error', "Failed to parse federation metadata : $!");
return undef;
}
$self->{'param_out'}{'federation_metadata_as_hashref'} = $federation_metadata->{'federation_metadata_as_hashref'};
return 1;
}
package IdPAccountManager::SAMLMetadata;
use strict;
use IdPAccountManager::Tools;
use IdPAccountManager::Conf;
use XML::LibXML;
require Exporter;
my @ISA = qw(Exporter);
my @EXPORT = qw();
use Carp;
sub new {
my ($pkg) = shift;
my %args = @_;
my $self = {};
## Bless SAMLMetadata object
bless $self, $pkg;
return $self;
}
## Load metadata
sub load {
my $self = shift;
my %in = @_;
unless ($in{'federation_metadata_file_path'}) {
&IdPAccountManager::Tools::do_log('error', "Missing parameter 'federation_metadata_file_path'");
return undef;
}
$self->{'federation_metadata_file_path'} = $in{'federation_metadata_file_path'};
unless (-r $self->{'federation_metadata_file_path'}) {
&IdPAccountManager::Tools::do_log('error', "Failed to read $in{'federation_metadata_file_path'} : $!");
return undef;
}
unless ($self->{'federation_metadata_as_xml'} = &_get_xml_object($in{'federation_metadata_file_path'})) {
&IdPAccountManager::Tools::do_log('error', "Failed to parse file $in{'metadata_file'} : $!");
return undef;
}
my $root = $self->{'federation_metadata_as_xml'}->documentElement();
unless ($root->nodeName() =~ /EntitiesDescriptor$/) {
&IdPAccountManager::Tools::do_log('error', "Root element of file $in{'federation_metadata_file_path'} is of type '%s'; should be 'EntitiesDescriptor'",
$root->nodeName());
return undef;
}
return 1;
}
## Parse XML structure of metadata to fill a hashref
sub parse {
my $self = shift;
$self->{'federation_metadata_as_hashref'} = &_parse_saml_metadata('metadata_as_xml' => $self->{'federation_metadata_as_xml'},
'filter_entity_type' => 'sp');
unless (defined $self->{'federation_metadata_as_hashref'}) {
&IdPAccountManager::Tools::do_log('error', "Failed to parse federation metadata");
return undef;
}
return 1;
}
## Print the content of a test account
sub print {
my $self = shift;
my $fd = shift || \*STDOUT;
my $root = $self->{'federation_metadata_as_xml'}->documentElement();
print $fd $root->toString();
return 1.
}
## Internal function
## returns a Lib::XML représenting an XML file
sub _get_xml_object {
my $metadata_file = shift;
&IdPAccountManager::Tools::do_log('debug', "");
unless (-f $metadata_file) {
&IdPAccountManager::Tools::do_log('error', "File $metadata_file not found: $!");
return undef;
}
unless (open FH, $metadata_file) {
&IdPAccountManager::Tools::do_log('error', "Failed to open file $metadata_file: $!");
return undef;
}
my $parser;
unless ($parser = XML::LibXML->new()) {
&IdPAccountManager::Tools::do_log('error', "Failed to initialize XML parser");
return undef;
}
$parser->line_numbers(1);
my $doc;
## Eval() prevents the parsing from killing the main process
eval {$doc = $parser->parse_fh(\*FH)};
if ($@) {
&IdPAccountManager::Tools::do_log('error', "Failed to parse file $metadata_file : $@");
return undef;
}
unless ($doc) {
&IdPAccountManager::Tools::do_log('error', "Failed to parse file $metadata_file : $!");
return undef;
}
return $doc;
}
## Parse a SAML federation metadata file
sub _parse_saml_metadata {
my %options = @_;
#&IdPAccountManager::Tools::do_log('trace', "%s", join(',',%options));
#unless ($options{'filter_entity_type'}) {
#&IdPAccountManager::Tools::do_log('error', "paramètre entity_type manquant");
#return undef;
#}
my $root = $options{'metadata_as_xml'};
my @extracted_array;
foreach my $EntityDescriptor (@{$root->getElementsByLocalName('EntityDescriptor')}) {
my $extracted_data = {};
if ($EntityDescriptor->hasAttributes()) {
foreach my $attr ($EntityDescriptor->getAttribute('entityID')) {
$extracted_data->{'entityid'} = $attr;
}
}
next if ($options{'filter_entity_id'} && ($options{'filter_entity_id'} ne $extracted_data->{'entityid'}));
#&IdPAccountManager::Tools::do_log('trace', "EntityId: %s - Cherche %s", $extracted_data->{'entityid'}, $options{'filter_entity_id'});
$extracted_data->{'xml_md'} = &IdPAccountManager::Tools::escape_xml($EntityDescriptor->toString());
#&IdPAccountManager::Tools::do_log('trace', "EntityId: %s", $extracted_data->{'entityid'});
#&IdPAccountManager::Tools::do_log('trace', "Entity dump: %s", $EntityDescriptor->toString());
foreach my $child ($EntityDescriptor->childNodes()) {
## Ignoringnodes of type XML::LibXML::Text or XML::LibXML::Comment
next unless (ref($child) =~ /^XML::LibXML::Element/);
if ($child->nodeName =~ /IDPSSODescriptor$/) {
$extracted_data->{'type'} = 'idp';
foreach my $sso ($child->getElementsByLocalName('SingleSignOnService')) {
## On ne prend en compte que les endpoints prévus
#next unless ($sso->getAttribute('Binding') && defined $supported_saml_bindings{$sso->getAttribute('Binding')});
## On extrait les infos sur les endpoints
push @{$extracted_data->{'idp_endpoints'}},
{'type' => 'SingleSignOnService',
'binding' => $sso->getAttribute('Binding'),
'location' => $sso->getAttribute('Location'),
};
}
## Getting domains declared for scoped attributes
foreach my $scope ($child->getElementsByLocalName('Scope')) {
push @{$extracted_data->{'domain'}}, $scope->textContent();
}
}elsif ($child->nodeName =~ /SPSSODescriptor$/) {
$extracted_data->{'type'} = 'sp';
## We check the Binding of the ACS that should match "urn:oasis:names:tc:SAML:1.0:profiles:browser-post"
## We also check the index to select the ACS that has the lower index
my ($index_saml1, $index_saml2);
foreach my $sso ($child->getElementsByLocalName('AssertionConsumerService')) {
## Extracting endpoints information
push @{$extracted_data->{'sp_endpoints'}},
{'type' => 'AssertionConsumerService',
'binding' => $sso->getAttribute('Binding'),
'location' => $sso->getAttribute('Location'),
'index' => $sso->getAttribute('index'),
'isdefault' => &IdPAccountManager::Tools::boolean2integer($sso->getAttribute('isDefault'))
};
#&IdPAccountManager::Tools::do_log('trace', "Endpoint: type:%s ; binding=%s ; location=%s ; index=%s ; isdefault=%s", 'AssertionConsumerService', $sso->getAttribute('Binding'), $sso->getAttribute('Location'), $sso->getAttribute('index'), $sso->getAttribute('isDefault'));
}
foreach my $requestedattribute ($child->getElementsByLocalName('RequestedAttribute')) {
## Requested attributes information
push @{$extracted_data->{'requested_attribute'}},
{'friendly_name' => &IdPAccountManager::Tools::encode_utf8($requestedattribute->getAttribute('FriendlyName')),
'name' => &IdPAccountManager::Tools::encode_utf8($requestedattribute->getAttribute('Name')),
'is_required' => &IdPAccountManager::Tools::boolean2integer($requestedattribute->getAttribute('isRequired'))
};
}
}elsif ($child->nodeName =~ /Extensions$/) {
#&IdPAccountManager::Tools::do_log('trace', "Extensions for %s", $extracted_data->{'entityid'});
foreach my $registrationinfo ($child->getElementsByLocalName('RegistrationInfo')) {
$extracted_data->{'registration_info'}{'registration_authority'} = $registrationinfo->getAttribute('registrationAuthority');
$extracted_data->{'registration_info'}{'registration_instant'} = $registrationinfo->getAttribute('registrationInstant');
foreach my $registrationpolicy ($registrationinfo->getElementsByLocalName('RegistrationPolicy')) {
if ($registrationpolicy->getAttribute('lang') eq 'en') {
$extracted_data->{'registration_info'}{'registration_policy'} = &IdPAccountManager::Tools::encode_utf8($registrationpolicy->textContent());
}
}
}
}elsif ($child->nodeName =~ /ContactPerson$/) {
my %contact_details;
$contact_details{'type'} = $child->getAttribute('contactType');
foreach my $contact_child ($child->childNodes()) {
$contact_details{$contact_child->nodeName} = &IdPAccountManager::Tools::encode_utf8($contact_child->textContent());
}
push @{$extracted_data->{'contacts'}}, \%contact_details;
}
foreach my $displayname ($child->getElementsByLocalName('DisplayName')) {
$extracted_data->{'display_name'}{$displayname->getAttribute('xml:lang')} = &IdPAccountManager::Tools::encode_utf8($displayname->textContent());
}
foreach my $description ($child->getElementsByLocalName('Description')) {
$extracted_data->{'description'}{$description->getAttribute('xml:lang')} = &IdPAccountManager::Tools::encode_utf8($description->textContent());
}
foreach my $contact ($child->getElementsByLocalName('ContactPerson')) {
&IdPAccountManager::Tools::do_log('trace', "ContactPerson");
my %contact_details;
$contact_details{'type'} = $contact->getAttribute('contactType');
foreach my $contact_child ($EntityDescriptor->childNodes()) {
&IdPAccountManager::Tools::do_log('trace', "Contact : %s", $contact_child->nodeName);
$contact_details{$contact_child->nodeName} = &IdPAccountManager::Tools::encode_utf8($contact_child->textContent());
}
push @{$extracted_data->{'contacts'}}, \%contact_details;
}
foreach my $sso ($child->getElementsByLocalName('OrganizationDisplayName')) {
$extracted_data->{'organization'} = &IdPAccountManager::Tools::encode_utf8($sso->textContent());
}
## Getting X.509 certificates
foreach my $cert ($child->getElementsByLocalName('X509Certificate')) {
$extracted_data->{'certificate'} = &IdPAccountManager::Tools::encode_utf8($cert->textContent());
}
}
## Filter entities based on type
#&IdPAccountManager::Tools::do_log('trace', "Entity type : %s", $extracted_data->{'type'});
next if (defined $options{'filter_entity_type'} &&
($options{'filter_entity_type'} ne $extracted_data->{'type'}));
## Merge domains in a single string
my $domains = join(',',@{$extracted_data->{'domain'}}) if ($extracted_data->{'domain'});
$extracted_data->{'domain'} = $domains;
#&IdPAccountManager::Tools::do_log('debug', "Scopes : %s", $domains);
push @extracted_array, $extracted_data;
}
return \@extracted_array;
}
1; # Magic true value required at end of module
__END__
=head1 NAME
SAMLMetadata - Perl module loading SAML federation metadata
=head1 SYNOPSIS
=head1 DESCRIPTION
=head1 SUBROUTINES/METHODS
=head1 AUTHOR
Olivier Salaün (olivier.salaun@renater.fr)
......@@ -2,6 +2,7 @@ package IdPAccountManager::Tools;
use Template;
use Digest::SHA;
use Encode;
my %log_levels = ('debug' => 0, 'info' => 1, 'trace' => 1, 'notice' => 2, 'error' => 3);
......@@ -168,7 +169,7 @@ sub mail_notice {
my $tt2 = Template->new(FILTERS => {qencode => [\&qencode, 0]});
unless ($tt2->process($tt2_file, $mail_data, \*SENDMAIL)) {
&do_log('error', "Erreur TT2 : %s", $tt2->error());
&do_log('error', "Error TT2 : %s", $tt2->error());
}
close SENDMAIL;
}
......@@ -190,6 +191,19 @@ sub encode_utf8 ($) {
return Encode::encode('utf8', $string);
}
## Escape characters that may interfer in an XML document
sub escape_xml {
my $s = shift;
$s =~ s/\&/&amp\;/gm;
$s =~ s/\"/&quot\;/gm;
$s =~ s/\</&lt\;/gm;
$s =~ s/\>/&gt\;/gm;
$s =~ s/\'/&#039;/gm;
return $s;
}
## usefull to pass parameters to TT2
sub escape_quotes {
my $string = shift;
......@@ -199,6 +213,19 @@ sub escape_quotes {
return $string;
}
## returns an integer (0 or 1), given an input string ('true' or 'false')
sub boolean2integer {
my $boolean = shift;
if ($boolean eq 'true') {
return 1;
}elsif ($boolean eq 'false') {
return 0;
}
return undef;
}
1; # Magic true value required at end of module
__END__
......
......@@ -13,7 +13,8 @@ L'URL des méta-données : [% provider.get('metadataurl') %]
Check logs for more details
[% ELSE %]
Error: [% error_type %]
Error: [% errors.join(',') %]
@IP : [% env.REMOTE_HOST %] / [% env.REMOTE_ADDR %]
Check logs for more details
[% END %]
......@@ -2,11 +2,18 @@
[% IF action == 'select_sp' %]
[% TRY %]
[% PROCESS 'templates/select_sp.tt2.html' %]
[% PROCESS 'templates/web/select_sp.tt2.html' %]
[% CATCH %]
An error occured
[% END %]
[% ELSIF action == 'get_sp_list' %]
[% TRY %]
[% PROCESS 'templates/web/get_sp_list.tt2.html' %]
[% CATCH %]
An error occured
[% END %]
[% ELSE %]
Error: unknown action
......
<div>
This Test Identity Provider allows you to create test accounts with different profiles to validate the behaviour of your own Service Provider registered in eduGAIN inter-federation. Note that only a Service Provider administrator can create accounts here.
<br/><br/>
Step 1: please select your Service Provider below
<form action="[% env.SCRIPT_NAME %]"
method="POST">
<select id="sp_entityid" name="sp_entityid">
<option value="">Select your Service Provider below</option>
[% FOREACH entity IN federation_metadata_as_hashref %]
<option value="[% entity.entityid %]">[% IF entity.display_name && entity.display_name.en %][% entity.display_name.en %] - [% END %][% entity.entityid %]</option>
[% END %]
</select>
<br/>
<input type="submit" name="action_select_sp" value="Next">
</form>
</div>
......@@ -81,21 +81,21 @@ div.important{border-style:solid;border-color:black;border-width:1px;background-
<div id="content">
[% IF erreurs %]
[% IF errors %]
<div class="ui-widget">
[% FOREACH err IN erreurs %]
[% FOREACH err IN errors %]
<p class="ui-state-error ui-corner-all" style="margin-top: 20px; padding: 0 .7em;"><span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>
[% IF lang == 'en' %]Error: [% ELSE %]Erreur : [% END %]
Error:
[% IF err == 'unknown_action' %]
[% IF lang == 'en' %]Unknown action[% ELSE %]action non supportée.[% END %]
Unknown action
[% ELSIF err == 'internal' %]
[% IF lang == 'en' %]internal error; administrators of the federation registry have been notified.[% ELSE %]erreur interne ; les administrateurs du guichet de la fédération ont été notifiés.[% END %]
internal error; administrators of the federation registry have been notified.
[% ELSIF (matches = err.match('missing_(\w+)')) %]
[% IF lang == 'en' %]missing parameter '[% matches.0 %]'[% ELSE %]paramètre '[% matches.0 %]' manquant.[% END %]
missing parameter '[% matches.0 %]'
[% ELSE %]
[% err %]
......@@ -112,11 +112,11 @@ div.important{border-style:solid;border-color:black;border-width:1px;background-
<div class="ui-widget">
[% FOREACH notif IN notifications %]
<p class="ui-state-highlight ui-corner-all" style="border: 2px solid #10427a; background: #DFF1EE;padding: 0.7em;"><span class="ui-icon ui-icon-alert" style="float: left; margin-right: .3em;"></span>
[% IF lang == 'en' %]Notice: [% ELSE %]Notification : [% END %]
Notice:
[% IF notif == 'done' %]
[% IF lang == 'en' %]Operation has been performed[% ELSE %]L'opération a été effectuée;[%END%]
Operation has been performed
[% ELSE %]
......@@ -130,7 +130,7 @@ div.important{border-style:solid;border-color:black;border-width:1px;background-
[% PROCESS 'templates/web/content.tt2.html' %]
[% END %] <!-- IF erreurs -->
[% END %] <!-- IF errors -->
<p>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment