diff --git a/conf/create-manager-db.sql b/conf/create-manager-db.sql index 43158aa5434bbc192f043b6783a8b74cc3d541d7..01112fb10c90ed24d420d3d9700bd6c22b596ea9 100644 --- a/conf/create-manager-db.sql +++ b/conf/create-manager-db.sql @@ -30,6 +30,8 @@ CREATE TABLE `services` ( CREATE TABLE `accounts` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, + `token` varchar(50) NOT NULL, + `password_crypt` varchar(50) NOT NULL, `password_hash` varchar(50) NOT NULL, `creation_date` datetime DEFAULT NULL, `expiration_date` datetime DEFAULT NULL, diff --git a/lib/AccountManager/Account.pm b/lib/AccountManager/Account.pm index 7417165676397a77058ea4e0a302833ef92f6221..f89f1202864c9c4d887c3f2efa254ef2be376bb6 100644 --- a/lib/AccountManager/Account.pm +++ b/lib/AccountManager/Account.pm @@ -12,7 +12,9 @@ __PACKAGE__->meta->setup( columns => [ id => { type => 'bigserial', not_null => 1 }, password_hash => { type => 'varchar', length => 50, not_null => 1 }, + password_crypt => { type => 'varchar', length => 50, not_null => 1 }, password => { type => 'varchar', length => 50, nonpersistent => 1 }, + token => { type => 'varchar', length => 50, not_null => 1 }, creation_date => { type => 'datetime' }, expiration_date => { type => 'datetime' }, profile => { type => 'varchar', length => 100, not_null => 1 }, diff --git a/lib/AccountManager/WebRequest.pm b/lib/AccountManager/WebRequest.pm index e2fa0b918ce6ce4973d0641854073929fa9105f4..ec9ee1016f545d0f2d1af4e1a0b6a2355061314a 100644 --- a/lib/AccountManager/WebRequest.pm +++ b/lib/AccountManager/WebRequest.pm @@ -6,13 +6,10 @@ use warnings; use CGI; use DateTime; use English qw(-no_match_vars); +use HTTP::AcceptLanguage; use Template; use Log::Any::Adapter; use List::MoreUtils qw(uniq); -use HTTP::AcceptLanguage; -use HTTP::Message; -use HTTP::Response; -use IO::String; use Text::CSV; use AccountManager::Account; @@ -40,6 +37,7 @@ my %actions = ( select_email => 'req_select_email', complete_challenge => 'req_complete_challenge', create_accounts => 'req_create_accounts', + download_accounts => 'req_download_accounts', ); ## New web request @@ -109,7 +107,8 @@ sub run { email => $parameters{email}, style => $parameters{style}, entityid => $parameters{entityid}, - token => $parameters{token} + token => $parameters{token}, + key => $parameters{key}, }; } @@ -158,7 +157,6 @@ sub respond { binmode(STDOUT, ":utf8"); print $self->{cgi}->header( - -nph => 1, -type => 'text/html', -charset => 'utf8' ); @@ -413,6 +411,11 @@ sub req_create_accounts { $self->respond({ errors => [ "missing_token" ] }); } + unless ($self->{in}->{email}) { + $self->{logger}->error("Missing parameter email"); + $self->respond({ errors => [ "missing_email" ] }); + } + my $token = AccountManager::Token->new( db => $self->{db}, token => $self->{in}->{token} @@ -455,6 +458,24 @@ sub req_create_accounts { $self->{configuration}->{$entity}->{account_validity_period} || $self->{configuration}->{service}->{account_validity_period}; + + my $download_token = AccountManager::Token->new( + db => $self->{db}, + email_address => $self->{in}->{email}, + sp_entityid => $self->{in}->{entityid}, + creation_date => DateTime->now(), + expiration_date => DateTime->now()->add(hours => $validity_period), + token => AccountManager::Tools::generate_secret(20) + ); + + unless ($download_token->save()) { + push @{ $self->{out}->{errors} }, "internal"; + $self->{logger}->error("Failed to save authentication token"); + $self->respond(); + } + + my $key = AccountManager::Tools::generate_secret(10); + foreach my $profile (split(/, */, $profiles)) { my $password = AccountManager::Tools::generate_password(10); my $account = AccountManager::Account->new( @@ -463,7 +484,9 @@ sub req_create_accounts { sp_entityid => $entity, scope => $self->{configuration}->{idp}->{scope}, password => $password, + password_crypt => AccountManager::Tools::encrypt($password, $key), password_hash => AccountManager::Tools::sha256_hash($password), + token => $download_token->token(), creation_date => DateTime->now(), expiration_date => DateTime->now()->add(days => $validity_period) ); @@ -505,55 +528,85 @@ sub req_create_accounts { $self->{in}->{token} ); - binmode(STDOUT, ":utf8"); + $self->respond({ + accounts => \@accounts, + entityid => $self->{in}->{entityid}, + key => $key, + token => $download_token->token(), + action => 'create_accounts' + }); +} - my $response = HTTP::Response->new( - 200, 'OK', [ 'Content-Type' => 'multipart/x-mixed-replace' ] - ); +sub req_download_accounts { + my ($self) = @_; - $response->protocol('HTTP/1.1'); - $response->date(time); - $response->server($ENV{SERVER_SOFTWARE}); + unless ($self->{in}->{entityid}) { + push @{ $self->{out}->{errors} }, "missing_entityid"; + $self->{logger}->error("Missing parameter entityid"); + $self->respond(); + } - # HTML page - my $data = { - app => { - name => $self->{configuration}->{app}->{name}, - url => $self->{configuration}->{app}->{url}, - support_email => $self->{configuration}->{app}->{support_email}, - version => $self->{configuration}->{app}->{version}, - }, - accounts => \@accounts, - accounts_validity_period => $self->{configuration}->{service}->{account_validity_period}, - idp_displayname => $self->{configuration}->{idp}->{displayname}, - entityid => $self->{in}->{entityid}, - action => 'create_accounts' - }; + unless ($self->{in}->{token}) { + push @{ $self->{out}->{errors} }, "missing_token"; + $self->{logger}->error("Missing parameter token"); + $self->respond(); + } - my $lang = HTTP::AcceptLanguage->new($ENV{HTTP_ACCEPT_LANGUAGE})->match(qw/en fr/) || 'en'; + unless ($self->{in}->{key}) { + push @{ $self->{out}->{errors} }, "missing_key"; + $self->{logger}->error("Missing parameter key"); + $self->respond(); + } - my $tt2 = Template->new({ - ENCODING => 'utf8', - INCLUDE_PATH => $self->{configuration}->{_}->{templates_dir} . "/web/$lang" - }); + my $token = AccountManager::Token->new( + db => $self->{db}, + token => $self->{in}->{token} + ); + + if (! $token->load(speculative => 1)) { + push @{ $self->{out}->{errors} }, "wrong_token"; + $self->{logger}->errorf( + "Non-existing authentication token %s", + $self->{in}->{token}, + ); + $self->respond(); + } - my $page_content; - $tt2->process('index.tt2.html', $data, \$page_content); + if (! $token->sp_entityid() eq $self->{in}->{entityid}) { + push @{ $self->{out}->{errors} }, "wrong_token_for_sp"; + $self->{logger}->errorf( + "Authentication token %s cannot be used for SP %s", + $self->{in}->{token}, + $self->{in}->{entityid} + ); + $self->respond(); + } - my $page_response = HTTP::Message->new( - [ - 'Content-Type' => 'text/html; charset=utf-8', - 'Content-Disposition' => 'inline' + # delete the token + unless ($token->delete()) { + $self->{logger}->errorf( + "Failed to delete authentication token %s", + $self->{in}->{token} + ); + } + + # load accounts from database + my $accounts = AccountManager::Account::Manager->get_accounts( + db => $self->{db}, + query => [ + token => $self->{in}->{token} ], - $page_content ); - $response->add_part($page_response); - # CSV file - my $csv = Text::CSV->new({ binary => 1, eol => "\r\n", quote_space => 0 }); - my $file_content; - my $file_content_io = IO::String->new($file_content); - $csv->print($file_content_io, [ qw/ + binmode(STDOUT, ":utf8"); + + print $self->{cgi}->header( + -type => 'text/csv', + -content_disposition => 'attachment; filename="accounts.csv"' + ); + + my $csv = Text::CSV->new ({ binary => 1, eol => "\r\n", quote_space => 0 }); + $csv->print(\*STDOUT, [ qw/ username password profile @@ -568,8 +621,13 @@ sub req_create_accounts { schacHomeOrganizationType / ]); - foreach my $account (@accounts) { - $csv->print($file_content_io, [ + foreach my $account (@$accounts) { + my $password = AccountManager::Tools::decrypt( + $account->password_crypt(), + $self->{in}->{key} + ); + $account->password($password); + $csv->print(\*STDOUT, [ $account->internal_uid(), $account->password(), $account->profile(), @@ -584,17 +642,6 @@ sub req_create_accounts { $account->schacHomeOrganizationType(), ]); } - - my $file_response = HTTP::Message->new( - [ - 'Content-Type' => 'text/csv; charset=utf-8', - 'Content-Disposition' => 'attachment; filename="accounts.csv"' - ], - $file_content - ); - $response->add_part($file_response); - - print $response->as_string(); } ## Return the homepage of the service diff --git a/templates/web/en/create_accounts.tt2.html b/templates/web/en/create_accounts.tt2.html index a93abf4531de9dd471cfae89d91c4432b3019e1c..1c2c9a7b1a97f5fd2f489621ca93ba67eeec82f6 100644 --- a/templates/web/en/create_accounts.tt2.html +++ b/templates/web/en/create_accounts.tt2.html @@ -7,6 +7,8 @@ do so, select <strong>[% idp_displayname %]</strong> when choosing an identity provider.</p> +<p>Click <a href="[% conf.app_url %]?action=download_accounts&entityid=[% entityid %]&token=[% token %]&key=[% key %]">here</a> to download the list of those accounts in CSV format.</p> + <div class="accounts_profile"> [% FOREACH account IN accounts %] <div class="tbl"> diff --git a/templates/web/en/select_sp.tt2.html b/templates/web/en/select_sp.tt2.html index fd5cedc6845964327c9496d20fefc881b0343737..f4a4f90ea4ca1612b86c52717a9926532d974cb6 100644 --- a/templates/web/en/select_sp.tt2.html +++ b/templates/web/en/select_sp.tt2.html @@ -44,7 +44,8 @@ jQuery(document).ready(function($){ if (currentIndex === 2 && newIndex === 3) { window.location="[% app.url %]?action=create_accounts&entityid="+ - encodeURIComponent($('#entityid').val())+"&token="+encodeURIComponent($('#token').val()); + encodeURIComponent($('#entityid').val())+"&token="+encodeURIComponent($('#token').val()) + + "&email="+encodeURIComponent($('#email').val()); } // Allways allow previous action even if the current form is not valid! diff --git a/templates/web/fr/create_accounts.tt2.html b/templates/web/fr/create_accounts.tt2.html index 37d2ca04d5a125e8530c03ffe4469f3d62d34a05..8d87f173a3af398baf5fe139525043a19ea02447 100644 --- a/templates/web/fr/create_accounts.tt2.html +++ b/templates/web/fr/create_accounts.tt2.html @@ -7,6 +7,8 @@ service fédéré. Pour le faire, sélectionnez <strong>[% idp_displayname %]</strong> lors du choix du founisseur d'identité à utiliser.</p> +<p>Cliquez <a href="[% conf.app_url %]?action=download_accounts&entityid=[% entityid %]&token=[% token %]&key=[% key %]">ici</a> pour télécharger la liste de ces comptes au format CSV.</p> + <div class="accounts_profile"> [% FOREACH account IN accounts %] <div class="tbl"> diff --git a/templates/web/fr/select_sp.tt2.html b/templates/web/fr/select_sp.tt2.html index a1ae369f40aa5f5139a1774b017b0ac91e1300f5..afb0b3ff4cd324421ea837712434a764722e9c7a 100644 --- a/templates/web/fr/select_sp.tt2.html +++ b/templates/web/fr/select_sp.tt2.html @@ -44,7 +44,8 @@ jQuery(document).ready(function($){ if (currentIndex === 2 && newIndex === 3) { window.location="[% app.url %]?action=create_accounts&entityid="+ - encodeURIComponent($('#entityid').val())+"&token="+encodeURIComponent($('#token').val()); + encodeURIComponent($('#entityid').val())+"&token="+encodeURIComponent($('#token').val()) + + "&email="+encodeURIComponent($('#email').val()); } // Allways allow previous action even if the current form is not valid!