diff --git a/bin/account-manager-web.pl b/bin/account-manager-web.pl new file mode 100755 index 0000000000000000000000000000000000000000..d04e310a47ed7e968bfb9924f974d888c821c88f --- /dev/null +++ b/bin/account-manager-web.pl @@ -0,0 +1,211 @@ +#!/usr/bin/perl + +## 15/09/2014, Olivier Salaün +## Web interface for the eduGAIN Test IdP Account Manager +## TODO : rename %erreurs e + +use strict; +use utf8; +use lib "/opt/testidp/IdPAccountManager/lib"; + +use CGI; +use CGI::Cookie; +use CGI::Util; +use Template; +use Template::Constants qw( :debug ); +use POSIX; + +use IdPAccountManager::TestAccount; + +## 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, + ); + +my %actions = ('select_sp' => {'title_en' => 'Select your Service Provider' } + ); + +## Gives writes for the group +umask 0002; + +chdir $IdPAccountManager::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 = {}; + + 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'} = \%IdPAccountManager::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'} = 'help'; + } + + 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 %{$request->{'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'}{'erreurs'}}, "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'}{'erreurs'}}, "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; + + ## 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]}, + #DEBUG => 'all', + #DEBUG => 'caller', + #DEBUG => 'parser' + }); + + my $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 @erreurs_admin; + foreach my $id_erreur (@{$self->{'param_out'}{'erreurs'}}) { + unless ($id_erreur =~ /^(error_x)$/) { + push @erreurs_admin, $id_erreur; + } + } + + ## Mail notification of admins about the error + if (@erreurs_admin) { + &IdPAccountManager::Tools::mail_notice('template' => 'templates/mail/notification_generic_error.tt2.eml', + 'data' => $self->{'param_out'}); + } + +} \ No newline at end of file diff --git a/lib/IdPAccountManager/Tools.pm b/lib/IdPAccountManager/Tools.pm index 620d2d3ed10fde754965bbce7c0045024c6466d1..98b1a0948901c11add17b02bea285a5fc10f65c5 100644 --- a/lib/IdPAccountManager/Tools.pm +++ b/lib/IdPAccountManager/Tools.pm @@ -135,6 +135,70 @@ sub do_log { return 1; } +## Send a mail notice +## Default is to send email to the manager admins, unless other recipients are specified +## mail_notice(IN) +## IN is a HASH with expected entries : +## template : mail template file +## data : data used by the TT2 parser +sub mail_notice { + my %in = @_; + my $tt2_file = $in{'template'}; + my $mail_data = $in{'data'}; + + $mail_data->{'conf'} ||= \%IdPAccountManager::Conf::global; + + my $notice_email = $in{'to'} || $IdPAccountManager::Conf::global{'admin_email'}; + $mail_data->{'to'} = $notice_email; + + ## Protection to prevent notifications during test dev phases + ## Notify only adresses @renater.fr + if ($IdPAccountManager::Conf::global{'no_mail_outside'}) { + foreach my $email (split /,/, $notice_email) { + unless ($email =~ /\@(cru|renater)\.fr$/) { + &do_log('error',"Notification to an external address skipped"); + return undef; + } + } + } + + &do_log('trace', '(template=%s, to=%s)', $in{'template'}, $mail_data->{'to'}); + + open SENDMAIL, "|/usr/sbin/sendmail -f ".$IdPAccountManager::Conf::global{'notice_from'}." $notice_email"; + + my $tt2 = Template->new(FILTERS => {qencode => [\&qencode, 0]}); + unless ($tt2->process($tt2_file, $mail_data, \*SENDMAIL)) { + &do_log('error', "Erreur TT2 : %s", $tt2->error()); + } + close SENDMAIL; +} + +sub qencode { + my $string = shift; + # We are not able to determine the name of header field, so assume + # longest (maybe) one. + return MIME::EncWords::encode_mimewords(Encode::decode('utf8', $string), + Encoding=>'A', + Charset=> 'utf8', + Field=>"subject"); +} + +## usefull to pass parameters to TT2 +sub encode_utf8 ($) { + my $string = shift||''; + + return Encode::encode('utf8', $string); +} + +## usefull to pass parameters to TT2 +sub escape_quotes { + my $string = shift; + + $string =~ s/\'/\\\'/g; + + return $string; +} + 1; # Magic true value required at end of module __END__ diff --git a/resources/geant_logo_rgb_300dpi.jpg b/resources/geant_logo_rgb_300dpi.jpg new file mode 100644 index 0000000000000000000000000000000000000000..820b7dee541a942c219abcbcf329b189e4433a96 Binary files /dev/null and b/resources/geant_logo_rgb_300dpi.jpg differ diff --git a/templates/mail/notification_generic_error.tt2.eml b/templates/mail/notification_generic_error.tt2.eml new file mode 100644 index 0000000000000000000000000000000000000000..9908628a5886a4be15b3a70480d029c4a450df13 --- /dev/null +++ b/templates/mail/notification_generic_error.tt2.eml @@ -0,0 +1,19 @@ +From: [% conf.app_name %] <[% conf.notice_from %]> +To: [% to %] +Subject: [% subject %] +Content-type: text/plain; charset=UTF-8; format=flowed + +You receive this notification as administrator of the eduGAIN Test Account Manager +An error occured during test account processing. + +[% IF error_type == 'skipping_provider_metadata_issue' %] +Le fichier de méta-données pour le provider [% entityid %] a un format incorrect ou n'a pas pu être téléchargé. + +L'URL des méta-données : [% provider.get('metadataurl') %] + +Check logs for more details +[% ELSE %] +Error: [% error_type %] + +Check logs for more details +[% END %] diff --git a/templates/web/content.tt2.html b/templates/web/content.tt2.html new file mode 100644 index 0000000000000000000000000000000000000000..16963a7ebced6cf34be2dc9e4e05d3603b5a7514 --- /dev/null +++ b/templates/web/content.tt2.html @@ -0,0 +1,13 @@ + + +[% IF action == 'select_sp' %] + [% TRY %] + [% PROCESS 'templates/select_sp.tt2.html' %] + [% CATCH %] + An error occured + [% END %] + +[% ELSE %] +Error: unknown action + +[% END %] diff --git a/templates/web/index.tt2.html b/templates/web/index.tt2.html new file mode 100644 index 0000000000000000000000000000000000000000..84f8a4381b8bc49bc9ce50ccf266a0786efe76ec --- /dev/null +++ b/templates/web/index.tt2.html @@ -0,0 +1,153 @@ +Content-Type: text/html +[% IF cookie %]Set-Cookie: [% cookie %][% END %] + +<?xml version="1.0" encoding="utf-8" ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/x +html1/DTD/xhtml1-transitional.dtd"> +<html xml:lang="[% iso639 = locale.match('^(.)(.)'); iso639.0; iso639.1 %]" xmlns="http://www.w3.org/1999/xhtml"> + +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8" /> + +<link type="text/css" href="/jquery/jquery-ui-1.8.16/css/smoothness/jquery-ui-1.8.16.custom.css" rel="Stylesheet" /> +<link rel="stylesheet" media="screen" type="text/css" href="/css/renater.css" /> +<link rel="icon" type="image/png" href="/images/favicon.png" /> + +<script type="text/javascript" src="/jquery/jquery-1.7.min.js"></script> +<script type="text/javascript" src="/jquery/jquery-ui-1.8.16.custom.min.js"></script> + +<SCRIPT TYPE="text/javascript"> +<!-- + + // To confirm on a link (A HREF) + function request_confirm_link(my_url, my_message) { + question = confirm(my_message); + if (question !="0") { + top.location = my_url; + } + } + +function showhide(div){ + var oDiv = document.getElementById(div); + if(oDiv.style.display == "none"){ + oDiv.style.display = "block"; + }else{ + oDiv.style.display = "none"; + } +} + +function hide(div) { + var oDiv = document.getElementById(div); + oDiv.style.display = "none"; +} + +//--> +</SCRIPT> + +<STYLE type="text/css"><!-- + +.login {float: left} +.menu {float: right} +.footer {text-align: center} +.prod {background-color: #028A34} +h1 {text-align: center} +li,dd {margin-left:10px;} +li.parametre {margin-bottom: 15px;} +dd.parametre {margin-bottom: 15px;} +div.important{border-style:solid;border-color:black;border-width:1px;background-color:#F5DEB3;padding:5px;} +.mandatory{color:#e00853;font-style:italic;font-weight:bold;margin:0 3px} +.notice{border:2px solid #05a;padding:5px 5px 5px 5px;margin:20px;} +--></STYLE> +<title> [% PROCESS 'templates/web/title.tt2.html' %] </title> + +</head> + +<body> + +<div id="wrapper"> + <div id="header"> + <div id="bandeau"> + <div class="logo"> + <img alt="GEANT logo" width="150" src="/resources/geant_logo_rgb_300dpi.jpg"/> + </div> + <div class="connection"> + </div> + + <div class="title"> + [% PROCESS 'templates/web/title.tt2.html' %] + </div> + + </div> + + + <div id="content"> + +[% IF erreurs %] +<div class="ui-widget"> + [% FOREACH err IN erreurs %] + +<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 %] + + [% IF err == 'unknown_action' %] + [% IF lang == 'en' %]Unknown action[% ELSE %]action non supportée.[% END %] + + [% 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 %] + + [% ELSIF (matches = err.match('missing_(\w+)')) %] + [% IF lang == 'en' %]missing parameter '[% matches.0 %]'[% ELSE %]paramètre '[% matches.0 %]' manquant.[% END %] + + [% ELSE %] + [% err %] + + [% END %] <!-- autorisation --> +</p> +<br/> + [% END %] <!-- FOREACH --> +</div> +[% ELSE %] + + +[% IF notifications %] +<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 %] + + [% IF notif == 'done' %] + + [% IF lang == 'en' %]Operation has been performed[% ELSE %]L'opération a été effectuée;[%END%] + + [% ELSE %] + + [% notif %]<br/> + + [% END %] + + [% END %] <!-- FOREACH --> +</b></div> +[% END %] <!-- notifications --> + +[% PROCESS 'templates/web/content.tt2.html' %] + +[% END %] <!-- IF erreurs --> + +<p> + +[% IF dump %] +DUMP :<br> + + [% USE Dumper %] + [% Dumper.dump_html(dump) %] +</p> +[% END %] + + <div> + <hr> + +<div align="middle">Test Account Manager [% conf.version %]</div> + </div> + </div> +</div> +</body> +</html> diff --git a/templates/web/title.tt2.html b/templates/web/title.tt2.html new file mode 100644 index 0000000000000000000000000000000000000000..80302a00d4cc7db07e8d7d40797d65b384318920 --- /dev/null +++ b/templates/web/title.tt2.html @@ -0,0 +1,6 @@ +[% conf.app_name %] - + + [% IF actions.$action %] + [% FILTER encode_utf8 %][% actions.$action.title %][% END %] + [% ELSE %] + [% END %]