#!/usr/bin/perl ## 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). ## 15/09/2014, Olivier Salaün ## Web interface for the eduGAIN Access Check Account Manager 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 ); use POSIX; use IdPAccountManager::TestAccount; 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, ); 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; ## 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"); $request->{'action'} = 'home'; } 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}$/) { 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 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; ## 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]}, 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'}); } } ## Return the list of known SPs first sub req_account_wizard { my $self = shift; &IdPAccountManager::Tools::do_log('info', ""); 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()) { 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; } ## 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; 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; &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; } ## First remove token if one exist for this email+SP 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", $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"; &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', ""); return 1; }