Newer
Older
use Template::Constants qw(:chomp);
use Text::CSV;
use AccountManager::Account;
use AccountManager::Account::Manager;
use AccountManager::Metadata;
use AccountManager::Service;
use AccountManager::Token;
use AccountManager::L10N;
# Format de type URL HTTP ou URN
my $entity_id_pattern = qr{
^
(?:
https?://[\w.:/-]+
|
urn:[\w.:-]+
)
$
}x;
my %actions = (
start => 'req_start',
select_federation => 'req_select_federation',
select_sp => 'req_select_sp',
select_email => 'req_select_email',
complete_challenge => 'req_complete_challenge',
create_accounts => 'req_create_accounts',
download_accounts => 'req_download_accounts',
configuration => $args{configuration},
bless $self, $pkg;
if ($self->{configuration}->{logger}) {
Log::Any::Adapter->set(
'File',
$self->{configuration}->{logger}->{file},
log_level => $self->{configuration}->{logger}->{level}
} else {
warn "no logger in configuration, logging disabled\n";
$self->{logger} = Log::Any->get_logger();
$self->{cgi} = CGI->new();
my $lang =
$self->{cgi}->param('lang') ||
$self->{cgi}->cookie('lang');
$self->{lh} = AccountManager::L10N->get_handle($lang ? $lang: ());
if (!$self->{lh}) {
$self->{logger}->fatal("Unable to get suitable language handle");
$self->respond(
template => 'errors.tt2.html',
data => {
errors => [ 'internal' ]
}
);
}
if (!$self->{configuration}->{mailer}) {
$self->{logger}->fatal(
"No mailer defined in configuration, aborting"
);
$self->respond(
data => {
errors => [ 'internal' ]
}
);
}
if (!$self->{configuration}->{idp}) {
$self->{logger}->fatal(
"No IDP defined in configuration, aborting"
);
$self->respond(
data => {
errors => [ 'internal' ]
}
);
}
if (!$self->{configuration}->{federations}) {
$self->{logger}->fatal(
"No federations defined in configuration, aborting"
);
$self->respond(
template => 'errors.tt2.html',
data => {
errors => [ 'internal' ]
}
);
}
if (!$self->{configuration}->{database}) {
$self->{logger}->fatal(
"No database defined in configuration, aborting"
);
$self->respond(
data => {
errors => [ 'internal' ]
}
);
} else {
AccountManager::DB->register_db(
driver => $self->{configuration}->{database}->{type},
database => $self->{configuration}->{database}->{name},
host => $self->{configuration}->{database}->{host},
username => $self->{configuration}->{database}->{username},
password => $self->{configuration}->{database}->{password},
options => [ split(/, */, $self->{configuration}->{database}->{options}) ]
);
# process input parameters
my %parameters = $self->{cgi}->Vars();
foreach my $parameter (keys %parameters) {
# cleanup
$parameters{$parameter} =~ s/\r//g; # remove &0D char
$parameters{$parameter} =~ s/\s+$//; # remove trailing spaces
$parameters{$parameter} =~ s/^\s+//; # remove leading spaces
# register needed parameters
email => $parameters{email},
entityid => $parameters{entityid},
token => $parameters{token},
key => $parameters{key},
federation => $parameters{federation},
# process requested action
my $action = $parameters{action} || 'home';
if ($actions{$action}) {
$self->{logger}->debug("Processing action '$action'");
my $method = $actions{$action};
$self->{logger}->error( "Unknown action '$action'");
errors => [ "Unknown action '$action'" ]
return 1;
}
## Return HTML content
sub respond {
my ($self, %in) = @_;
$in{data}->{app} = {
url => $ENV{SCRIPT_NAME},
support_email => $self->{configuration}->{app}->{support_email},
$in{data}->{lh} = $self->{lh};
ENCODING => 'utf8',
PRE_CHOMP => CHOMP_ONE,
INCLUDE_PATH => $self->{configuration}->{setup}->{templates_dir} . "/web"
$self->{logger}->debug("Responding with template '$in{template}'");
my $cookie = $self->{cgi}->cookie(
-name => 'lang',
-value => $self->{lh}->language_tag(),
-expires => undef,
);
print $self->{cgi}->header(
-type => 'text/html',
-charset => 'utf8',
-cookie => [ $cookie ]
unless ($tt2->process($in{template}, $in{data}, \*STDOUT)) {
printf "Content-type: text/plain\n\n Error: %s", $tt2->error();
$self->{logger}->errorf("Web parser error : %s", $tt2->error());
sub req_start {
my ($self) = @_;
my @federations = keys %{$self->{configuration}->{federations}};
if (@federations == 1) {
$self->{in}->{federation} = $federations[0];
$self->req_select_sp();
} else {
$self->req_select_federation();
}
}
sub req_select_federation {
my ($self) = @_;
my @federations = keys %{$self->{configuration}->{federations}};
$self->respond(
template => 'select_federation.tt2.html',
data => {
action => 'select_federation',
federations => \@federations
}
);
}
my $federation = $self->{in}->{federation};
if (!$federation) {
$self->{logger}->error("Missing parameter: federation");
$self->respond(
template => 'errors.tt2.html',
data => {
errors => [ "missing_federation" ]
}
);
}
my $file = $self->{configuration}->{federations}->{$federation};
if (!$file) {
$self->{logger}->error("Incorrect parameter: federation");
$self->respond(
template => 'errors.tt2.html',
data => {
errors => [ "invalid_federation" ]
}
);
}
$metadata = AccountManager::Metadata->new(
);
};
if ($EVAL_ERROR) {
$self->{logger}->error("Failed to load federation metadata: $EVAL_ERROR");
data => {
errors => [ "internal" ]
}
);
template => 'select_sp.tt2.html',
action => 'select_sp',
metadata => $metadata->parse(type => 'sp'),
federation => $federation,
my $federation = $self->{in}->{federation};
if (!$federation) {
$self->{logger}->error("Missing parameter: federation");
$self->respond(
template => 'errors.tt2.html',
data => {
errors => [ "missing_federation" ]
}
);
}
my $file = $self->{configuration}->{federations}->{$federation};
if (!$file) {
$self->{logger}->error("Incorrect parameter: federation");
$self->respond(
template => 'errors.tt2.html',
data => {
errors => [ "invalid_federation" ]
}
);
}
if (! $self->{in}->{entityid}) {
$self->{logger}->error("Missing parameter: entityid");
errors => [ "missing_entityid" ]
if ($self->{in}->{entityid} !~ $entity_id_pattern) {
$self->{logger}->error("Incorrect parameter format: entityid");
$self->respond(
data => {
errors => [ "format_entityid" ]
}
);
}
# Create a persistent service provider object
# already present in DB, nothing todo
} else {
# extract information from metadata
my $metadata;
eval {
$metadata = AccountManager::Metadata->new(
);
};
if ($EVAL_ERROR) {
$self->{logger}->error("Failed to load federation metadata: $EVAL_ERROR");
my $entities = $metadata->parse(id => $self->{in}->{entityid});
if (!@$entities) {
"No such SP '%s' in metadata", $self->{in}->{entityid}
$sp->displayname($entity->{display_name});
$sp->contacts(uniq map { $_->{EmailAddress} } @{$entity->{contacts}})
if $entity->{contacts};
$self->{logger}->error("Failed to save service provider object");
# override metadata contacts if needed
$self->{configuration}->{$id}->{contacts} ||
$self->{configuration}->{service}->{contacts};
if ($contacts) {
if ($contacts =~ /^\+(.+)/) {
# complement original contacts
$sp->contacts($sp->contacts(), split(/, */, $1));
} else {
# replace original contacts
template => 'select_email.tt2.html',
action => 'select_email',
federation => $federation,
sp => $sp,
entityid => $sp->entityid(),
my $federation = $self->{in}->{federation};
if (!$federation) {
$self->{logger}->error("Missing parameter: federation");
$self->respond(
template => 'errors.tt2.html',
data => {
errors => [ "missing_federation" ]
}
);
}
my $file = $self->{configuration}->{federations}->{$federation};
if (!$file) {
$self->{logger}->error("Incorrect parameter: federation");
$self->respond(
template => 'errors.tt2.html',
data => {
errors => [ "invalid_federation" ]
}
);
}
unless ($self->{in}->{entityid}) {
$self->{logger}->error("Missing parameter entityid");
data => {
errors => [ "missing_entityid" ]
}
);
unless ($self->{in}->{email}) {
$self->{logger}->error("Missing parameter email");
my $provider = AccountManager::Service->new(
unless ($provider->load(speculative => 1)) {
$self->{logger}->errorf("No such SP '%s' in database", $self->{in}->{entityid});
# override metadata contacts if needed
my $contacts =
$self->{configuration}->{$entity}->{contacts} ||
$self->{configuration}->{service}->{contacts};
if ($contacts) {
if ($contacts =~ /^\+(.+)/) {
# complement original contacts
$provider->contacts($provider->contacts(), split(/, */, $1));
} else {
# replace original contacts
$provider->contacts(split(/, */, $contacts));
}
}
## Check that email is a known contact for this SP
unless ($provider->is_contact($self->{in}->{email}))
$self->{logger}->errorf(
"Requested a token for %s for an unautorized address '%s'",
$self->{in}->{entityid},
$self->{in}->{email}
# delete any previous token for the same email/service couple
my $old_token = AccountManager::Token->new(
email_address => $self->{in}->{email},
sp_entityid => $self->{in}->{entityid}
if ($old_token->load(speculative => 1)) {
unless ($old_token->delete()) {
$self->{logger}->errorf(
"Failed to delete previous authentication token with ID %s",
$self->{configuration}->{service}->{tokens_validity_period};
my $token = AccountManager::Token->new(
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)
$self->{logger}->error("Failed to save authentication token");
# build content
my $content;
ENCODING => 'utf8',
PRE_CHOMP => CHOMP_ONE,
INCLUDE_PATH => $self->{configuration}->{setup}->{templates_dir} . "/mail",
my $template = 'send_authentication_token.tt2.txt';
app => {
url => $self->{configuration}->{app}->{url},
support_email => $self->{configuration}->{app}->{support_email},
version => $self->{configuration}->{app}->{version},
to => $self->{in}->{email},
entityid => $self->{in}->{entityid},
token => $token->token(),
$tt2->process($template, $data, \$content);
# wrap content in message
my $message = MIME::Lite->new(
From => sprintf('eduGAIN Access Check <%s>', $self->{configuration}->{mailer}->{from}),
To => $self->{in}->{email},
Subject => encode("MIME-Header", sprintf('[eduGAIN Access Check] %s', $self->{lh}->maketext("Test accounts request"))),
Type =>'text/plain',
Data => $content,
);
# send message
eval {
$message->send_by_sendmail();
};
if ($EVAL_ERROR) {
$self->{logger}->errorf("Mail notification error: %s", $EVAL_ERROR);
errors => [ "mail_notification_failure" ]
$self->{logger}->infof(
"Token send to %s for entityid=%s;token=%s",
$self->{in}->{email},
$self->{in}->{entityid},
template => 'complete_challenge.tt2.html',
action => 'complete_challenge',
federation => $federation,
entityid => $self->{in}->{entityid},
email => $self->{in}->{email},
unless ($self->{in}->{entityid}) {
$self->{logger}->error("Missing parameter entityid");
errors => [ "missing_entityid" ]
unless ($self->{in}->{token}) {
$self->{logger}->error("Missing parameter token");
data => {
errors => [ "missing_token" ]
}
);
unless ($self->{in}->{email}) {
$self->{logger}->error("Missing parameter email");
}
my $token = AccountManager::Token->new(
if (! $token->load(speculative => 1)) {
$self->{logger}->errorf(
"Failed to validate authentication token %s for entityid %s",
$self->{in}->{token},
$self->{in}->{entityid}
if (! $token->sp_entityid() eq $self->{in}->{entityid}) {
$self->{logger}->errorf(
"Authentication token %s cannot be used for SP with entityid %s",
$self->{in}->{token},
$self->{in}->{entityid}
errors => [ "wrong_token_for_sp" ]
}
## delete the token
unless ($token->delete()) {
$self->{logger}->errorf(
"Failed to delete authentication token %s",
);
}
## create test accounts
my $profiles =
$self->{configuration}->{$entity}->{account_profiles} ||
$self->{configuration}->{service}->{account_profiles};
my $validity_period =
$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()) {
$self->{logger}->error("Failed to save authentication token");
}
my $key = AccountManager::Tools::generate_secret(10);
foreach my $profile (split(/, */, $profiles)) {
my $password = AccountManager::Tools::generate_password(10);
my $account = AccountManager::Account->new(
db => $self->{db},
profile => $profile,
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)
$self->{logger}->errorf(
"Failed to create test accounts for SP with entityid %s",
errors => [ "accounts_creation_failure" ]
}
## Update simpleSAMLphp configuration to enable test accounts
my $accounts = AccountManager::Account::Manager->get_accounts(
db => $self->{db}
);
AccountManager::Tools::update_ssp_authsources(
$self->{configuration}->{setup}->{templates_dir},
$self->{configuration}->{setup}->{accounts_file},
);
};
if ($EVAL_ERROR) {
$self->{logger}->errorf(
"Failed to create simpleSAMLphp configuration file: %s",
$EVAL_ERROR
errors => [ "accounts_creation_failed" ]
$self->{logger}->infof(
"Token validated for entityid=%s;token=%s",
$self->{in}->{entityid},
$self->{in}->{token}
template => 'create_accounts.tt2.html',
action => 'create_accounts',
accounts => \@accounts,
entityid => $self->{in}->{entityid},
email => $self->{in}->{email},
key => $key,
token => $download_token->token(),
sub req_download_accounts {
my ($self) = @_;
unless ($self->{in}->{entityid}) {
$self->{logger}->error("Missing parameter entityid");
errors => [ "missing_entityid" ]
unless ($self->{in}->{token}) {
$self->{logger}->error("Missing parameter token");
unless ($self->{in}->{key}) {
$self->{logger}->error("Missing parameter key");
my $token = AccountManager::Token->new(
db => $self->{db},
token => $self->{in}->{token}
);
if (! $token->load(speculative => 1)) {
$self->{logger}->errorf(
"Non-existing authentication token %s",
$self->{in}->{token},
);
if (! $token->sp_entityid() eq $self->{in}->{entityid}) {
$self->{logger}->errorf(
"Authentication token %s cannot be used for SP %s",
$self->{in}->{token},
$self->{in}->{entityid}
);
errors => [ "wrong_token_for_sp" ]
# 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}
],
);
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
cn
displayName
givenName
mail
eduPersonAffiliation
eduPersonScopedAffiliation
eduPersonPrincipalName
schacHomeOrganization
schacHomeOrganizationType
/ ]);
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(),
$account->cn(),
$account->displayName(),
$account->givenName(),
$account->mail(),
join(', ', $account->eduPersonAffiliation()),
join(', ', $account->eduPersonScopedAffiliation()),
$account->eduPersonPrincipalName(),
$account->schacHomeOrganization(),
$account->schacHomeOrganizationType(),
]);
}
}
## Return the homepage of the service
sub req_home {
data => {
action => 'home'
}