Skip to content
Snippets Groups Projects
Commit 916c258e authored by Guillaume ROUSSE's avatar Guillaume ROUSSE
Browse files

drop multipart response, allow distinct accounts download instead

parent b9a67ff9
No related branches found
No related tags found
No related merge requests found
...@@ -30,6 +30,8 @@ CREATE TABLE `services` ( ...@@ -30,6 +30,8 @@ CREATE TABLE `services` (
CREATE TABLE `accounts` ( CREATE TABLE `accounts` (
`id` bigint(20) NOT NULL AUTO_INCREMENT, `id` bigint(20) NOT NULL AUTO_INCREMENT,
`token` varchar(50) NOT NULL,
`password_crypt` varchar(50) NOT NULL,
`password_hash` varchar(50) NOT NULL, `password_hash` varchar(50) NOT NULL,
`creation_date` datetime DEFAULT NULL, `creation_date` datetime DEFAULT NULL,
`expiration_date` datetime DEFAULT NULL, `expiration_date` datetime DEFAULT NULL,
......
...@@ -12,7 +12,9 @@ __PACKAGE__->meta->setup( ...@@ -12,7 +12,9 @@ __PACKAGE__->meta->setup(
columns => [ columns => [
id => { type => 'bigserial', not_null => 1 }, id => { type => 'bigserial', not_null => 1 },
password_hash => { type => 'varchar', length => 50, 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 }, password => { type => 'varchar', length => 50, nonpersistent => 1 },
token => { type => 'varchar', length => 50, not_null => 1 },
creation_date => { type => 'datetime' }, creation_date => { type => 'datetime' },
expiration_date => { type => 'datetime' }, expiration_date => { type => 'datetime' },
profile => { type => 'varchar', length => 100, not_null => 1 }, profile => { type => 'varchar', length => 100, not_null => 1 },
......
...@@ -6,13 +6,10 @@ use warnings; ...@@ -6,13 +6,10 @@ use warnings;
use CGI; use CGI;
use DateTime; use DateTime;
use English qw(-no_match_vars); use English qw(-no_match_vars);
use HTTP::AcceptLanguage;
use Template; use Template;
use Log::Any::Adapter; use Log::Any::Adapter;
use List::MoreUtils qw(uniq); use List::MoreUtils qw(uniq);
use HTTP::AcceptLanguage;
use HTTP::Message;
use HTTP::Response;
use IO::String;
use Text::CSV; use Text::CSV;
use AccountManager::Account; use AccountManager::Account;
...@@ -40,6 +37,7 @@ my %actions = ( ...@@ -40,6 +37,7 @@ my %actions = (
select_email => 'req_select_email', select_email => 'req_select_email',
complete_challenge => 'req_complete_challenge', complete_challenge => 'req_complete_challenge',
create_accounts => 'req_create_accounts', create_accounts => 'req_create_accounts',
download_accounts => 'req_download_accounts',
); );
## New web request ## New web request
...@@ -109,7 +107,8 @@ sub run { ...@@ -109,7 +107,8 @@ sub run {
email => $parameters{email}, email => $parameters{email},
style => $parameters{style}, style => $parameters{style},
entityid => $parameters{entityid}, entityid => $parameters{entityid},
token => $parameters{token} token => $parameters{token},
key => $parameters{key},
}; };
} }
...@@ -158,7 +157,6 @@ sub respond { ...@@ -158,7 +157,6 @@ sub respond {
binmode(STDOUT, ":utf8"); binmode(STDOUT, ":utf8");
print $self->{cgi}->header( print $self->{cgi}->header(
-nph => 1,
-type => 'text/html', -type => 'text/html',
-charset => 'utf8' -charset => 'utf8'
); );
...@@ -413,6 +411,11 @@ sub req_create_accounts { ...@@ -413,6 +411,11 @@ sub req_create_accounts {
$self->respond({ errors => [ "missing_token" ] }); $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( my $token = AccountManager::Token->new(
db => $self->{db}, db => $self->{db},
token => $self->{in}->{token} token => $self->{in}->{token}
...@@ -455,6 +458,24 @@ sub req_create_accounts { ...@@ -455,6 +458,24 @@ sub req_create_accounts {
$self->{configuration}->{$entity}->{account_validity_period} || $self->{configuration}->{$entity}->{account_validity_period} ||
$self->{configuration}->{service}->{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)) { foreach my $profile (split(/, */, $profiles)) {
my $password = AccountManager::Tools::generate_password(10); my $password = AccountManager::Tools::generate_password(10);
my $account = AccountManager::Account->new( my $account = AccountManager::Account->new(
...@@ -463,7 +484,9 @@ sub req_create_accounts { ...@@ -463,7 +484,9 @@ sub req_create_accounts {
sp_entityid => $entity, sp_entityid => $entity,
scope => $self->{configuration}->{idp}->{scope}, scope => $self->{configuration}->{idp}->{scope},
password => $password, password => $password,
password_crypt => AccountManager::Tools::encrypt($password, $key),
password_hash => AccountManager::Tools::sha256_hash($password), password_hash => AccountManager::Tools::sha256_hash($password),
token => $download_token->token(),
creation_date => DateTime->now(), creation_date => DateTime->now(),
expiration_date => DateTime->now()->add(days => $validity_period) expiration_date => DateTime->now()->add(days => $validity_period)
); );
...@@ -505,55 +528,85 @@ sub req_create_accounts { ...@@ -505,55 +528,85 @@ sub req_create_accounts {
$self->{in}->{token} $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( sub req_download_accounts {
200, 'OK', [ 'Content-Type' => 'multipart/x-mixed-replace' ] my ($self) = @_;
);
$response->protocol('HTTP/1.1'); unless ($self->{in}->{entityid}) {
$response->date(time); push @{ $self->{out}->{errors} }, "missing_entityid";
$response->server($ENV{SERVER_SOFTWARE}); $self->{logger}->error("Missing parameter entityid");
$self->respond();
}
# HTML page unless ($self->{in}->{token}) {
my $data = { push @{ $self->{out}->{errors} }, "missing_token";
app => { $self->{logger}->error("Missing parameter token");
name => $self->{configuration}->{app}->{name}, $self->respond();
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'
};
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({ my $token = AccountManager::Token->new(
ENCODING => 'utf8', db => $self->{db},
INCLUDE_PATH => $self->{configuration}->{_}->{templates_dir} . "/web/$lang" 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; if (! $token->sp_entityid() eq $self->{in}->{entityid}) {
$tt2->process('index.tt2.html', $data, \$page_content); 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( # delete the token
[ unless ($token->delete()) {
'Content-Type' => 'text/html; charset=utf-8', $self->{logger}->errorf(
'Content-Disposition' => 'inline' "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 binmode(STDOUT, ":utf8");
my $csv = Text::CSV->new({ binary => 1, eol => "\r\n", quote_space => 0 });
my $file_content; print $self->{cgi}->header(
my $file_content_io = IO::String->new($file_content); -type => 'text/csv',
$csv->print($file_content_io, [ qw/ -content_disposition => 'attachment; filename="accounts.csv"'
);
my $csv = Text::CSV->new ({ binary => 1, eol => "\r\n", quote_space => 0 });
$csv->print(\*STDOUT, [ qw/
username username
password password
profile profile
...@@ -568,8 +621,13 @@ sub req_create_accounts { ...@@ -568,8 +621,13 @@ sub req_create_accounts {
schacHomeOrganizationType schacHomeOrganizationType
/ ]); / ]);
foreach my $account (@accounts) { foreach my $account (@$accounts) {
$csv->print($file_content_io, [ my $password = AccountManager::Tools::decrypt(
$account->password_crypt(),
$self->{in}->{key}
);
$account->password($password);
$csv->print(\*STDOUT, [
$account->internal_uid(), $account->internal_uid(),
$account->password(), $account->password(),
$account->profile(), $account->profile(),
...@@ -584,17 +642,6 @@ sub req_create_accounts { ...@@ -584,17 +642,6 @@ sub req_create_accounts {
$account->schacHomeOrganizationType(), $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 ## Return the homepage of the service
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
do so, select <strong>[% idp_displayname %]</strong> when choosing an identity do so, select <strong>[% idp_displayname %]</strong> when choosing an identity
provider.</p> 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"> <div class="accounts_profile">
[% FOREACH account IN accounts %] [% FOREACH account IN accounts %]
<div class="tbl"> <div class="tbl">
......
...@@ -44,7 +44,8 @@ jQuery(document).ready(function($){ ...@@ -44,7 +44,8 @@ jQuery(document).ready(function($){
if (currentIndex === 2 && newIndex === 3) if (currentIndex === 2 && newIndex === 3)
{ {
window.location="[% app.url %]?action=create_accounts&entityid="+ 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! // Allways allow previous action even if the current form is not valid!
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
service fédéré. Pour le faire, sélectionnez <strong>[% idp_displayname service fédéré. Pour le faire, sélectionnez <strong>[% idp_displayname
%]</strong> lors du choix du founisseur d'identité à utiliser.</p> %]</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"> <div class="accounts_profile">
[% FOREACH account IN accounts %] [% FOREACH account IN accounts %]
<div class="tbl"> <div class="tbl">
......
...@@ -44,7 +44,8 @@ jQuery(document).ready(function($){ ...@@ -44,7 +44,8 @@ jQuery(document).ready(function($){
if (currentIndex === 2 && newIndex === 3) if (currentIndex === 2 && newIndex === 3)
{ {
window.location="[% app.url %]?action=create_accounts&entityid="+ 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! // Allways allow previous action even if the current form is not valid!
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment