Newer
Older
package IdPAccountManager::WebRequest;
## New web request
sub new {
my $pkg = shift;
format => $args{format},
actions => $args{actions},
};
$self->{logger} = IdPAccountManager::Logger->new(
file => $Conf::global{'log_file'},
verbosity => $Conf::global{'log_level'}
);
level => LOG_INFO,
message => ''
);
my $http_query = new CGI;
## Input parameters
my %in_vars = $http_query->Vars;
## Check if admin acts as another user
#if (defined $self->{'cookies'}{'as_user'} && $request->{'is_admin'}) {
# $self->{'utilisateur'} = $request->{'as_user'} = $request->{'cookies'}{'as_user'}->value;
# $self->{'is_admin'} = 0;
#}
## Usefull data for output (web pages or mail notices)
$self->{'param_out'}{'url_cgi'} = $ENV{'SCRIPT_NAME'};
$self->{'param_out'}{'env'} = \%ENV;
$self->{'param_out'}{'actions'} = $args{actions};
$self->{'param_out'}{'conf'} = \%Conf::global;
#open TMP, ">/tmp/account_manager.in"; IdPAccountManager::Tools::dump_var($self->{'param_in'}, 0, \*TMP); close TMP;
foreach my $key (keys %{ $self->{'param_in'} }) {
#$self->{logger}->log(level => LOG_TRACE, message => "PARAM_ENTREE: %s=%s", $key, $self->{'param_in'}{$key});
$self->{'param_in'}{$key} =~ s/\s+$//; ## Remove trailing spaces
$self->{'param_in'}{$key} =~ s/^\s+//; ## Remove leading spaces
#if ($self->{'param_in'}{$key} =~ /\0/) {
# my @valeurs = split /\0/, $self->{'param_in'}{$key};
# $self->{'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+)$/) {
#$self->{logger}->log(level => LOG_TRACE, message => "ACTION $key");
}
}
## Check the requested action
if ($self->{'param_in'}{'action'}) {
$self->{'action'} = $self->{'param_in'}{'action'};
} else {
## Default action
$self->{logger}->log(level => LOG_INFO, message => 'Default action');
$self->{'action'} = 'home';
}
## Execute a web request
sub execute {
my $self = shift;
$self->{logger}->log(level => LOG_DEBUG, message => "");
my $status;
## Check input parameters format
foreach my $key (keys %{ $self->{'param_in'} }) {
if ( $self->{'param_in'}{$key} !~ /^\s*$/
&& defined $self->{format}->{$key}
&& !ref($self->{format}->{$key}))
unless ($self->{'param_in'}{$key} =~ /^$self->format->{$key}$/) {
push @{ $self->{'param_out'}{'errors'} }, "format_$key";
$self->{logger}->log(
level => LOG_ERROR,
message => "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 $self->actions->{ $self->{'action'} }) {
## Execute the target subroutine named req_actionName
no strict 'refs';
my $sub = 'req_' . $self->{'action'};
$status = &{$sub}($self);
} else {
## Inknown action
push @{ $self->{'param_out'}{'errors'} }, "unknown_action";
$self->{logger}->log(
level => LOG_ERROR,
message => "Unknown action '%s'",
$self->{'action'}
);
}
} while ($self->{'next_action'});
#return undef if (!defined $status);
return 1;
}
## Return HTML content
sub respond {
my $self = shift;
$self->{logger}->log(level => LOG_DEBUG, message => "");
## 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}) {
#$self->{logger}->log(level => LOG_TRACE, message => "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'}) {
#$self->{logger}->log(level => LOG_TRACE, message => "URL Redirect : $self->{'url_redirection'}");
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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 ]
},
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();
$self->{logger}->log(
level => LOG_ERROR,
message => sprintf("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'},
'logger' => $self->{'logger'}
);
}
}
## Return the list of known SPs first
sub req_account_wizard {
my $self = shift;
$self->{logger}->log(level => LOG_INFO, message => "");
my $federation_metadata = new IdPAccountManager::SAMLMetadata(
logger => $self->{logger}
);
unless (
$federation_metadata->load(
federation_metadata_file_path =>
$Conf::global{'federation_metadata_file_path'}
)
)
{
push @{ $self->{'param_out'}{'errors'} }, "internal";
$self->{logger}->log(
level => LOG_ERROR,
message => "Failed to load federation metadata : $!"
);
return undef;
}
unless ($federation_metadata->parse()) {
push @{ $self->{'param_out'}{'errors'} }, "internal";
$self->{logger}->log(
level => LOG_ERROR,
message => "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;
$self->{logger}->log(level => LOG_INFO, message => "");
unless ($self->{'param_in'}{'sp_entityid'}) {
push @{ $self->{'param_out'}{'errors'} }, "missing_sp_entityid";
$self->{logger}
->log(level => LOG_ERROR, message => "Missing parameter sp_entityid");
my $federation_metadata = new IdPAccountManager::SAMLMetadata(
logger => $self->{logger}
);
unless (
$federation_metadata->load(
federation_metadata_file_path =>
$Conf::global{'federation_metadata_file_path'}
)
)
{
push @{ $self->{'param_out'}{'errors'} }, "internal";
$self->{logger}->log(
level => LOG_ERROR,
message => "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";
$self->{logger}->log(
level => LOG_ERROR,
message => "Failed to parse federation metadata : $!"
);
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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
return undef;
}
## 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'} })
{
$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";
$self->{logger}->log(
level => LOG_ERROR,
message => "Failed to create serviceprovider object"
);
return undef;
}
}
unless ($service_provider->save()) {
push @{ $self->{'param_out'}{'errors'} }, "internal";
$self->{logger}->log(
level => LOG_ERROR,
message => "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;
return 1;
}
## 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;
$self->{logger}->log(level => LOG_INFO, message => "");
unless ($self->{'param_in'}{'sp_entityid'}) {
push @{ $self->{'param_out'}{'errors'} }, "missing_sp_entityid";
$self->{logger}
->log(level => LOG_ERROR, message => "Missing parameter sp_entityid");
return undef;
}
unless ($self->{'param_in'}{'email_address'}) {
push @{ $self->{'param_out'}{'errors'} }, "email_address";
$self->{logger}->log(
level => LOG_ERROR,
message => "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";
$self->{logger}->log(
level => LOG_ERROR,
message => "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";
$self->{logger}->log(
level => LOG_ERROR,
message =>
"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";
$self->{logger}->log(
level => LOG_ERROR,
message => "Failed to create authentication token"
);
return undef;
}
## First remove token if one exist for this email+SP
if ($authentication_token->load()) {
unless ($authentication_token->delete()) {
push @{ $self->{'param_out'}{'errors'} }, "internal";
$self->{logger}->log(
level => LOG_ERROR,
message => sprintf(
"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";
$self->{logger}->log(
level => LOG_ERROR,
message => "Failed to create authentication token"
);
return undef;
}
}
unless ($authentication_token->save()) {
push @{ $self->{'param_out'}{'errors'} }, "internal";
$self->{logger}->log(
level => LOG_ERROR,
message => "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'},
'logger' => $self->{'logger'}
$self->{logger}->log(
level => LOG_INFO,
message => "Token send to %s for sp_entityid=%s;token=%s",
$self->{'param_in'}{'email_address'},
$self->{'param_in'}{'sp_entityid'},
$self->{'param_out'}{'authentication_token'}
);
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;
$self->{logger}->log(level => LOG_INFO, message => "");
unless ($self->{'param_in'}{'sp_entityid'}) {
push @{ $self->{'param_out'}{'errors'} }, "missing_sp_entityid";
$self->{logger}
->log(level => LOG_ERROR, message => "Missing parameter sp_entityid");
return undef;
}
unless ($self->{'param_in'}{'authentication_token'}) {
push @{ $self->{'param_out'}{'errors'} },
"missing_authentication_token";
$self->{logger}->log(
level => LOG_ERROR,
message => "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";
$self->{logger}->log(
level => LOG_ERROR,
message =>
"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";
$self->{logger}->log(
level => LOG_ERROR,
message =>
"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()) {
$self->{logger}->log(
level => LOG_ERROR,
message => "Failed to delete authentication token %s",
$self->{'param_in'}{'authentication_token'}
);
}
## 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";
$self->{logger}->log(
level => LOG_ERROR,
message => "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";
$self->{logger}->log(
level => LOG_ERROR,
message => "Failed to create simpleSAMLphp configuration file"
);
$self->{logger}->log(
level => LOG_INFO,
message => "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;
$self->{logger}->log(level => LOG_INFO, message => "");
return 1;
}
1;