Newer
Older
renater.salaun
committed
## Copyright (c) GEANT
## This software was developed by RENATER. The research leading to these results has received funding
## from the European Community¹s Seventh Framework Programme (FP7/2007-2013) under grant agreement nº 238875 (GÉANT).
## Web interface for the eduGAIN Access Check Account Manager
renater.salaun
committed
use strict vars;
use utf8;
use lib "/opt/testidp/IdPAccountManager/lib";
use lib "/opt/testidp/IdPAccountManager/conf";
use CGI;
use CGI::Cookie;
use CGI::Util;
use Template;
use Template::Constants qw( :debug );
renater.salaun
committed
use POSIX;
use IdPAccountManager::TestAccount;
renater.salaun
committed
use IdPAccountManager::SAMLMetadata;
use IdPAccountManager::ServiceProvider;
use IdPAccountManager::AuthenticationToken;
## Defining parameters format
my $urn_or_url_regex = '(http(s?):\/\/|urn:)[^\\\$\*\"\'\`\^\|\<\>\n\s]+'; ## Format de type URL HTTP ou URN
my $url_regex = 'http(s?):\/\/[^\\\$\*\"\'\`\^\|\<\>\n\s]+';
my $email_regex = '([\w\-\_\.\/\+\=\'\&]+|\".*\")\@[\w\-]+(\.[\w\-]+)+';
my $domains_regex = '[\w\.\-]+(,[\w\.\-]+)*';
my %format = (
## URL
#'attributeauthority' => $url_regex,
'sp_entityid' => $urn_or_url_regex,
renater.salaun
committed
my %actions = ('select_sp' => {'title_en' => 'Select your Service Provider' },
'account_wizard' => {'title_en' => 'Select your Service Provider' },
'generate_token' => {'title_en' => 'Generate an authentication token'},
'validate_token' => {'title_en' => 'Complete Email Challenge'},
'home' => {'title_en' => $Conf::global{'app_name'}},
);
## Gives writes for the group
umask 0002;
chdir $Conf::global{'root_manager_dir'};
my $request = new WebRequest;
if (defined $request) {
$request->execute();
}
$request->respond();
package WebRequest;
## New web request
sub new {
my $pkg = shift;
my $request = {};
&IdPAccountManager::Tools::do_log('info', "");
my $http_query = new CGI;
## Input parameters
my %in_vars = $http_query->Vars;
$request->{'param_in'} = \%in_vars;
## Check if admin acts as another user
$request->{'cookies'} = CGI::Cookie->fetch;
#if (defined $request->{'cookies'}{'as_user'} && $request->{'is_admin'}) {
# $request->{'utilisateur'} = $request->{'as_user'} = $request->{'cookies'}{'as_user'}->value;
# $request->{'is_admin'} = 0;
#}
## Usefull data for output (web pages or mail notices)
$request->{'param_out'}{'url_cgi'} = $ENV{'SCRIPT_NAME'};
$request->{'param_out'}{'env'} = \%ENV;
$request->{'param_out'}{'actions'} = \%actions;
$request->{'param_out'}{'conf'} = \%Conf::global;
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
## Dumping input data
#open TMP, ">/tmp/account_manager.in"; &IdPAccountManager::Tools::dump_var($request->{'param_in'}, 0, \*TMP); close TMP;
## Clean input vars
foreach my $key (keys %{$request->{'param_in'}}) {
#&IdPAccountManager::Tools::do_log('trace', "PARAM_ENTREE: %s=%s", $key, $request->{'param_in'}{$key});
## Removing all ^M (0D)
$request->{'param_in'}{$key} =~ s/\r//g;
$request->{'param_in'}{$key} =~ s/\s+$//; ## Remove trailing spaces
$request->{'param_in'}{$key} =~ s/^\s+//; ## Remove leading spaces
#if ($request->{'param_in'}{$key} =~ /\0/) {
# my @valeurs = split /\0/, $request->{'param_in'}{$key};
# $request->{'param_in'}{$key} = $valeurs[0]; ## Only keep first value of multi-valued parameters
#}
## If action_xx param is set, then action=xx
## Usefull to have sementicless values in submit forms
if ($key =~ /^action_(\w+)$/) {
#&IdPAccountManager::Tools::do_log('trace', "ACTION $key");
$request->{'param_in'}{'action'} = $1;
}
}
## Check the requested action
if ($request->{'param_in'}{'action'} ) {
$request->{'action'} = $request->{'param_in'}{'action'};
}else {
## Default action
&IdPAccountManager::Tools::do_log('info', "Default action");
}
bless $request, $pkg;
return $request;
}
## Execute a web request
sub execute {
my $self = shift;
&IdPAccountManager::Tools::do_log('debug', "");
my $status;
## Check input parameters format
foreach my $key (keys %{$self->{'param_in'}}) {
if ($self->{'param_in'}{$key} !~ /^\s*$/ &&
defined $format{$key} &&
! ref($format{$key})) {
unless ($self->{'param_in'}{$key} =~ /^$format{$key}$/) {
renater.salaun
committed
push @{$self->{'param_out'}{'errors'}}, "format_$key";
&IdPAccountManager::Tools::do_log('error', "Incorrect parameter format : $key");
return undef;
}
}
}
do {
## Actions can be chained
$self->{'action'} = $self->{'next_action'} if ($self->{'next_action'});
delete $self->{'next_action'}; ## Prevent loops
if (defined $actions{$self->{'action'}}) {
## Execute the target subroutine named req_actionName
my $sub = 'req_'.$self->{'action'};
$status = &{$sub}($self);
}else {
## Inknown action
renater.salaun
committed
push @{$self->{'param_out'}{'errors'}}, "unknown_action";
&IdPAccountManager::Tools::do_log('error', "Unknown action '%s'", $self->{'action'});
}
} while ($self->{'next_action'});
#return undef if (!defined $status);
return 1;
}
## Return HTML content
sub respond {
my $self = shift;
&IdPAccountManager::Tools::do_log('debug', "");
## Dump output data
#open TMP, ">/tmp/account_registry.out"; &IdPAccountManager::Tools::dump_var($self->{'param_out'}, 0, \*TMP); close TMP;
renater.salaun
committed
## 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");
$self->{'param_out'}{$key} ||= $self->{$key} unless ($key eq 'param_out');
}
## An action may redirect to an external URL
if ($self->{'url_redirection'}) {
#&IdPAccountManager::Tools::do_log('trace', "URL Redirect : $self->{'url_redirection'}");
printf "Location: %s\n\n", $self->{'url_redirection'};
}else {
#$self->{'param_out'}{'cookie'} = CGI::Cookie->new(-name=>'as_user',-value=>$self->{'as_user'},-expires=>'-1M');
## Parse template
my $tt2 = Template->new({
ENCODING => 'iso-8859-1', ## le défaut apparemment
FILTERS => {'encode_utf8', => [\&IdPAccountManager::Tools::encode_utf8, 0],
'escape_quotes' => [\&IdPAccountManager::Tools::escape_quotes, 0]},
renater.salaun
committed
INCLUDE_PATH => $Conf::global{'root_manager_dir'}.':'.$Conf::global{'root_manager_dir'}.'/templates/accountProfiles',
#DEBUG => 'all',
#DEBUG => 'caller',
#DEBUG => 'parser'
});
my $template;
## nobanner is used to do AJAX to get only pieces of HTML to load in the web client
if ($self->{'param_in'}{'style'} eq 'nobanner') {
$template = 'templates/web/index-nobanner.tt2.html';
}else {
$template = 'templates/web/index.tt2.html';
}
unless ($tt2->process($template, $self->{'param_out'}, \*STDOUT)) {
printf "Content-type: text/plain\n\n Error: %s", $tt2->error();
&IdPAccountManager::Tools::do_log('error', "Web parser error : %s", $tt2->error());
}
}
## Ignore some type of errors
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 (@errors_admin) {
$self->{'param_out'}{'subject'} = 'Error notification - web interface';
&IdPAccountManager::Tools::mail_notice('template' => 'templates/mail/notification_generic_error.tt2.eml',
'data' => $self->{'param_out'});
}
renater.salaun
committed
}
## Return the list of known SPs first
sub req_account_wizard {
renater.salaun
committed
my $self = shift;
&IdPAccountManager::Tools::do_log('info', "");
renater.salaun
committed
my $federation_metadata = new IdPAccountManager::SAMLMetadata;
unless ($federation_metadata->load(federation_metadata_file_path => $Conf::global{'federation_metadata_file_path'})) {
renater.salaun
committed
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;
}
## Select a Service Provider and return metadata sctucture for the SP
## Sample URL : https://dev-edugain.renater.fr/accountmanager?action=select_sp&sp_entityid=http%3A%2F%2Fsp.lat.csc.fi
sub req_select_sp {
my $self = shift;
&IdPAccountManager::Tools::do_log('info', "");
unless ($self->{'param_in'}{'sp_entityid'}) {
push @{$self->{'param_out'}{'errors'}}, "missing_sp_entityid";
&IdPAccountManager::Tools::do_log('error', "Missing parameter sp_entityid");
return undef;
}
my $federation_metadata = new IdPAccountManager::SAMLMetadata;
unless ($federation_metadata->load(federation_metadata_file_path => $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(filter_entity_id => $self->{'param_in'}{'sp_entityid'})) {
push @{$self->{'param_out'}{'errors'}}, "internal";
&IdPAccountManager::Tools::do_log('error', "Failed to parse federation metadata : $!");
return undef;
}
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
## Create a serviceprovider object to store major parameters for this SP in DB
my $service_provider = new IdPAccountManager::ServiceProvider(entityid => $self->{'param_in'}{'sp_entityid'});
## Prepare data
#open TMP, ">/tmp/account_manager_metadata.dump"; &IdPAccountManager::Tools::dump_var($federation_metadata->{'federation_metadata_as_hashref'}[0], 0, \*TMP); close TMP;
my $sp_metadata_as_hashref = $federation_metadata->{'federation_metadata_as_hashref'}[0];
my @contacts;
if (defined $sp_metadata_as_hashref->{'contacts'}) {
foreach my $contact (@{$sp_metadata_as_hashref->{'contacts'}}) {
my $email = $contact->{'EmailAddress'};
$email =~ s/^(mailto:)//; ## Remove 'mailto:' prefixes if any
push @contacts, $email;
}
}
my $display_name;
if (defined $sp_metadata_as_hashref->{'display_name'}) {
## Use English version of displayName if available
if ($sp_metadata_as_hashref->{'display_name'}{'en'}) {
$display_name = $sp_metadata_as_hashref->{'display_name'}{'en'};
## Else any language
}else {
foreach my $lang (keys %{$sp_metadata_as_hashref->{'display_name'}}) {
#&IdPAccountManager::Tools::do_log('TRACE', "Display name(%s): %s", $lang, $sp_metadata_as_hashref->{'display_name'}{$lang});
$display_name = $sp_metadata_as_hashref->{'display_name'}{$lang};
last;
}
}
}
## Try loading DB object first
if ($service_provider->load(speculative => 1)) {
$service_provider->contacts(join(',', @contacts));
$service_provider->displayname($display_name);
}else {
$service_provider = new IdPAccountManager::ServiceProvider(entityid => $self->{'param_in'}{'sp_entityid'},
contacts => join(',', @contacts),
displayname => $display_name);
unless (defined $service_provider) {
push @{$self->{'param_out'}{'errors'}}, "internal";
&IdPAccountManager::Tools::do_log('error', "Failed to create serviceprovider object");
return undef;
}
}
unless ($service_provider->save()) {
push @{$self->{'param_out'}{'errors'}}, "internal";
&IdPAccountManager::Tools::do_log('error', "Failed to save serviceprovider object");
return undef;
}
$self->{'param_out'}{'sp_metadata_as_hashref'} = $federation_metadata->{'federation_metadata_as_hashref'}[0];
$self->{'param_out'}{'serviceprovider'} = $service_provider;
## Generate an authentication token to validate an email address
## Sample call : dev-edugain.renater.fr/accountmanager?action=generate_token&style=nobanner&sp_entityid=https%3A%2F%2Fsourcesup.cru.fr%2Fshibboleth&email_address=support%40renater.fr
sub req_generate_token {
my $self = shift;
&IdPAccountManager::Tools::do_log('info', "");
unless ($self->{'param_in'}{'sp_entityid'}) {
push @{$self->{'param_out'}{'errors'}}, "missing_sp_entityid";
&IdPAccountManager::Tools::do_log('error', "Missing parameter sp_entityid");
return undef;
}
unless ($self->{'param_in'}{'email_address'}) {
push @{$self->{'param_out'}{'errors'}}, "email_address";
&IdPAccountManager::Tools::do_log('error', "Missing parameter email_address");
return undef;
}
## Create a serviceprovider object to load parameters for this SP from DB
my $service_provider = new IdPAccountManager::ServiceProvider(entityid => $self->{'param_in'}{'sp_entityid'});
# Try loading DB object first
unless ($service_provider->load(speculative => 1)) {
push @{$self->{'param_out'}{'errors'}}, "internal";
&IdPAccountManager::Tools::do_log('error', "Failed to load SP with entityid '%s'", $self->{'param_in'}{'sp_entityid'});
return undef;
}
## Check that email_address is a known contact for this SP
unless ($service_provider->is_contact($self->{'param_in'}{'email_address'})) {
push @{$self->{'param_out'}{'errors'}}, "internal";
&IdPAccountManager::Tools::do_log('error', "Requested a token for %s for an unautorized address '%s'", $self->{'param_in'}{'sp_entityid'}, $self->{'param_in'}{'email_address'});
return undef;
}
my $authentication_token = new IdPAccountManager::AuthenticationToken('email_address' => $self->{'param_in'}{'email_address'},
'sp_entityid' => $self->{'param_in'}{'sp_entityid'});
unless (defined $authentication_token) {
push @{$self->{'param_out'}{'errors'}}, "internal";
&IdPAccountManager::Tools::do_log('error', "Failed to create authentication token");
return undef;
}
if ($authentication_token->load()) {
unless ($authentication_token->delete()) {
push @{$self->{'param_out'}{'errors'}}, "internal";
&IdPAccountManager::Tools::do_log('error', "Failed to delete previous authentication token with ID %s", $authentication_token->get('id'));
return undef;
}
$authentication_token = new IdPAccountManager::AuthenticationToken('email_address' => $self->{'param_in'}{'email_address'},
'sp_entityid' => $self->{'param_in'}{'sp_entityid'});
unless (defined $authentication_token) {
push @{$self->{'param_out'}{'errors'}}, "internal";
&IdPAccountManager::Tools::do_log('error', "Failed to create authentication token");
return undef;
}
}
unless ($authentication_token->save()) {
push @{$self->{'param_out'}{'errors'}}, "internal";
&IdPAccountManager::Tools::do_log('error', "Failed to save authentication token");
return undef;
}
$self->{'param_out'}{'authentication_token'} = $authentication_token->get('token');
$self->{'param_out'}{'email_address'} = $self->{'param_in'}{'email_address'};
$self->{'param_out'}{'sp_entityid'} = $self->{'param_in'}{'sp_entityid'};
$self->{'param_out'}{'to'} = $self->{'param_in'}{'email_address'};
## Send the challenge email with the token
&IdPAccountManager::Tools::mail_notice('template' => 'templates/mail/send_authentication_token.tt2.eml',
'to' => $self->{'param_in'}{'email_address'},
'data' => $self->{'param_out'});
return 1;
}
## Validate an authentication token
## Test accounts get created
## Sample call : dev-edugain.renater.fr/accountmanager?action=validate_token&style=nobanner&sp_entityid=https%3A%2F%2Fsourcesup.cru.fr%2Fshibboleth&authentication_token=c1cfecb51ea40d39a695
sub req_validate_token {
my $self = shift;
&IdPAccountManager::Tools::do_log('info', "");
unless ($self->{'param_in'}{'sp_entityid'}) {
push @{$self->{'param_out'}{'errors'}}, "missing_sp_entityid";
&IdPAccountManager::Tools::do_log('error', "Missing parameter sp_entityid");
return undef;
}
unless ($self->{'param_in'}{'authentication_token'}) {
push @{$self->{'param_out'}{'errors'}}, "missing_authentication_token";
&IdPAccountManager::Tools::do_log('error', "Missing parameter authentication_token");
return undef;
}
my $authentication_token = new IdPAccountManager::AuthenticationToken(token => $self->{'param_in'}{'authentication_token'});
unless ($authentication_token->load()) {
push @{$self->{'param_out'}{'errors'}}, "wrong_token";
&IdPAccountManager::Tools::do_log('error', "Failed to validate authentication token %s for sp_entityid %s",
$self->{'param_in'}{'authentication_token'}, $self->{'param_in'}{'sp_entityid'});
return undef;
}
unless ($authentication_token->get('sp_entityid') eq $self->{'param_in'}{'sp_entityid'}) {
push @{$self->{'param_out'}{'errors'}}, "wrong_token_for_sp";
&IdPAccountManager::Tools::do_log('error', "Authentication token %s cannot be used for SP with entityid %s",
$self->{'param_in'}{'authentication_token'}, $self->{'param_in'}{'sp_entityid'});
return undef;
}
## delete the token
unless ($authentication_token->delete()) {
&IdPAccountManager::Tools::do_log('error', "Failed to delete authentication token %s",
}
## create test accounts
my @test_accounts = &IdPAccountManager::TestAccount::create_test_accounts_for_sp(sp_entityid => $self->{'param_in'}{'sp_entityid'});
unless (@test_accounts) {
push @{$self->{'param_out'}{'errors'}}, "accounts_creation_failed";
&IdPAccountManager::Tools::do_log('error', "Failed to create test accounts for SP with entityid %s",
$self->{'param_in'}{'sp_entityid'});
return undef;
}
## Update simpleSAMLphp configuration to enable test accounts
unless (&IdPAccountManager::Tools::update_ssp_authsources()) {
push @{$self->{'param_out'}{'errors'}}, "accounts_creation_failed";
&IdPAccountManager::Tools::do_log('error', "Failed to create simpleSAMLphp configuration file");
return undef;
}
&IdPAccountManager::Tools::do_log('info', "Token validated for sp_entityid=%s;token=%s", $self->{'param_in'}{'sp_entityid'}, $self->{'param_in'}{'authentication_token'});
$self->{'param_out'}{'sp_entityid'} = $self->{'param_in'}{'sp_entityid'};
$self->{'param_out'}{'test_accounts'} = \@test_accounts;
return 1;
}
## Return the homepage of the service
sub req_home {
my $self = shift;
&IdPAccountManager::Tools::do_log('info', "");