Newer
Older
use Template::Constants qw(:chomp);
use AccountManager::Account;
use AccountManager::Metadata;
use AccountManager::L10N;
# Format de type URL HTTP ou URN
my %patterns = (
entityid => 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 => $self->{configuration}->{database}->{options} ?
[ split(/, */, $self->{configuration}->{database}->{options}) ] : undef,
# process requested action

Guillaume ROUSSE
committed
my $action = $self->{cgi}->param('action') || 'home';
if ($actions{$action}) {
$self->{logger}->debug("Processing action '$action'");
my $method = $actions{$action};
$self->abort(
logs => "Unknown action '$action'",
user => "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 abort {
my $self = shift;
my %args = @_;
$self->{logger}->error($args{log}) if $args{log};
$self->respond(
template => 'errors.tt2.html',
data => {
errors => [ $args{user} ]
}
);
}
sub req_start {

Guillaume ROUSSE
committed
my ($self, %args) = @_;
my @federations = keys %{$self->{configuration}->{federations}};
if (@federations == 1) {

Guillaume ROUSSE
committed
$self->req_select_sp(federation => $federations[0]);
} else {
$self->req_select_federation();
}
}

Guillaume ROUSSE
committed
my ($self, %args) = @_;
my @federations = keys %{$self->{configuration}->{federations}};
$self->respond(
template => 'select_federation.tt2.html',
data => {
action => 'select_federation',
federations => \@federations
}
);
}

Guillaume ROUSSE
committed
my ($self, %args) = @_;
my $federation = $args{federation} ||
$self->get_parameter(name => 'federation');
my $metadata_file = $self->get_metadata_file(federation => $federation);
$metadata = AccountManager::Metadata->new(
$self->abort(
log => "Failed to load federation metadata: $EVAL_ERROR",
user => "internal"
) if $EVAL_ERROR;
template => 'select_sp.tt2.html',
action => 'select_sp',
metadata => $metadata->parse(type => 'sp'),
federation => $federation,

Guillaume ROUSSE
committed
my ($self, %args) = @_;
my $federation = $self->get_parameter(name => 'federation');
my $entityid = $self->get_parameter(name => 'entityid');
my $metadata_file = $self->get_metadata_file(federation => $federation);
# Create a persistent service provider object
my $sp = AccountManager::ServiceProvider->new(

Guillaume ROUSSE
committed
entityid => $entityid
# already present in DB, nothing todo
} else {
# extract information from metadata
my $metadata;
eval {
$metadata = AccountManager::Metadata->new(
$self->abort(
log => "Failed to load federation metadata: $EVAL_ERROR",
user => "internal"
) if $EVAL_ERROR;

Guillaume ROUSSE
committed
my $entities = $metadata->parse(id => $entityid);
log => "No such SP $entityid in metadata",
user => "no_such_entity"
) if !$entity;
$sp->displayname($entity->{display_name});
$sp->contacts(uniq map { $_->{EmailAddress} } @{$entity->{contacts}})
if $entity->{contacts};
log => "Failed to save service provider",
user => "internal"
) if !$sp->save();
# override metadata contacts if needed

Guillaume ROUSSE
committed
$self->{configuration}->{$entityid}->{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,

Guillaume ROUSSE
committed
entityid => $entityid,

Guillaume ROUSSE
committed
my ($self, %args) = @_;
my $federation = $self->get_parameter(name => 'federation');
my $entityid = $self->get_parameter(name => 'entityid');
my $email = $self->get_parameter(name => 'email');
my $metadata_file = $self->get_metadata_file(federation => $federation);

Guillaume ROUSSE
committed
entityid => $entityid,

Guillaume ROUSSE
committed
log => sprintf("No such SP '%s' in database", $entityid),
# override metadata contacts if needed

Guillaume ROUSSE
committed
$self->{configuration}->{$entityid}->{contacts} ||
$self->{configuration}->{service}->{contacts};
if ($contacts) {
if ($contacts =~ /^\+(.+)/) {
# complement original contacts
$sp->contacts($sp->contacts(), split(/, */, $1));
} else {
# replace original contacts
## Check that email is a known contact for this SP
log => "Requested a token for SP $entityid with unautorized address $email",
# delete any previous token for the same email/service couple
my $old_token = AccountManager::Token->new(

Guillaume ROUSSE
committed
email_address => $email,
sp_entityid => $entityid,
if ($old_token->load(speculative => 1)) {
log => "Failed to delete old authentication token",
user => "internal"
) if !$old_token->delete();
$self->{configuration}->{service}->{tokens_validity_period};
my $token = AccountManager::Token->new(

Guillaume ROUSSE
committed
email_address => $email,
sp_entityid => $entityid,
creation_date => DateTime->now(),
expiration_date => DateTime->now()->add(hours => $validity_period),
secret => AccountManager::Tools::generate_secret(20)
log => "Failed to save creation authentication token",
user => "internal"
) if !$token->save();
ENCODING => 'utf8',
PRE_CHOMP => CHOMP_ONE,
INCLUDE_PATH => $self->{configuration}->{setup}->{templates_dir} . "/mail",
app => {
url => $self->{configuration}->{app}->{url},
support_email => $self->{configuration}->{app}->{support_email},
version => $self->{configuration}->{app}->{version},

Guillaume ROUSSE
committed
to => $email,
entityid => $entityid,
token => $token->secret(),
challenge_url => sprintf(
'%s&action=complete_challenge&federation=%s&entity=%s&email=%s',
$self->{configuration}->{app}->{url},

Guillaume ROUSSE
committed
$federation,
$entityid,
$email,
my $text_content;
my $html_content;
$tt2->process('send_authentication_token.tt2.txt', $data, \$text_content);
$tt2->process('send_authentication_token.tt2.html', $data, \$html_content);
eval "require Email::MIME";
eval "require Email::Sender::Simple";

Guillaume ROUSSE
committed
my $message = Email::MIME->create(
header_str => [
'From' => sprintf('eduGAIN Access Check <%s>', $self->{configuration}->{mailer}->{from}),

Guillaume ROUSSE
committed
'To' => $email,
'Subject' => sprintf('[eduGAIN Access Check] %s', $self->{lh}->maketext("Test accounts request")),
'Content-Type' => 'multipart/alternative'
],
parts => [
Email::MIME->create(
attributes => {
content_type => "text/plain",
charset => 'utf-8',
encoding => 'quoted-printable'
},
body_str => $text_content
),
Email::MIME->create(
attributes => {
content_type => "text/html",
charset => 'utf-8',
encoding => 'quoted-printable'
},
body_str => $html_content
),
]

Guillaume ROUSSE
committed
Email::Sender::Simple->send($message);
$self->abort(
log => "Mail notification error: $EVAL_ERROR",
user => "mail_notification_failure"
) if $EVAL_ERROR;
$self->{logger}->infof(
"Token send to %s for entityid=%s;token=%s",

Guillaume ROUSSE
committed
$email,
$entityid,
template => 'complete_challenge.tt2.html',
action => 'complete_challenge',
federation => $federation,

Guillaume ROUSSE
committed
entityid => $entityid,
email => $email,

Guillaume ROUSSE
committed
my ($self, %args) = @_;
my $entityid = $self->get_parameter(name => 'entityid');
my $token = $self->get_parameter(name => 'token');
my $email = $self->get_parameter(name => 'email');
$self->check_token(token => $token, entityid => $entityid);
## create test accounts

Guillaume ROUSSE
committed
$self->{configuration}->{$entityid}->{account_profiles} ||
$self->{configuration}->{service}->{account_profiles};
my $validity_period =

Guillaume ROUSSE
committed
$self->{configuration}->{$entityid}->{account_validity_period} ||
$self->{configuration}->{service}->{account_validity_period};
my $download_token = AccountManager::Token->new(
db => $self->{db},

Guillaume ROUSSE
committed
email_address => $email,
sp_entityid => $entityid,
creation_date => DateTime->now(),
expiration_date => DateTime->now()->add(hours => $validity_period),
secret => AccountManager::Tools::generate_secret(20)
);
$self->abort(
log => "Failed to save download authentication token",
user => "internal"
) if !$download_token->save();
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,

Guillaume ROUSSE
committed
sp_entityid => $entityid,
scope => $self->{configuration}->{idp}->{scope},
password => $password,
password_crypt => AccountManager::Tools::encrypt($password, $key),
password_hash => AccountManager::Tools::sha256_hash($password),
token => $download_token->secret(),
creation_date => DateTime->now(),
expiration_date => DateTime->now()->add(days => $validity_period)
log => "Failed to create test accounts for SP $entityid",
user => "accounts_creation_failure"
) if !@accounts;
## Update simpleSAMLphp configuration to enable test accounts
my $accounts = AccountManager::Account->get_accounts(db => $self->{db});
AccountManager::Tools::update_ssp_authsources(
$self->{configuration}->{setup}->{templates_dir},
$self->{configuration}->{setup}->{accounts_file},
log => "Failed to create simpleSAMLphp configuration file: $EVAL_ERROR",
user => "accounts_creation_failure"
$self->{logger}->infof(

Guillaume ROUSSE
committed
$entityid,
template => 'create_accounts.tt2.html',
action => 'create_accounts',
accounts => \@accounts,

Guillaume ROUSSE
committed
entityid => $entityid,
email => $email,
token => $download_token->secret(),
sub req_download_accounts {
my ($self) = @_;
my $entityid = $self->get_parameter(name => 'entityid');
my $token = $self->get_parameter(name => 'token');
my $key = $self->get_parameter(name => 'key');
$self->check_token(token => $token, entityid => $entityid);
# load accounts from database
my $accounts = AccountManager::Account->get_accounts(
db => $self->{db},
query => [
],
);
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($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'
}
sub get_parameter {
my ($self, %args) = @_;
my $name = $args{name};
my $value = $self->{cgi}->param($name);
$self->abort(
log => "Missing parameter: $name",
user => "missing_$name"
) if !$value;
if ($patterns{$name}) {
$self->abort(
log => "Incorrect parameter format: $name",
user => "format_$name"
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
) if $value !~ $patterns{$name};
}
return $value;
}
sub get_metadata_file {
my ($self, %args) = @_;
my $federation = $args{federation};
my $file = $self->{configuration}->{federations}->{$federation};
$self->abort(
log => "Incorrect parameter: federation",
user => "invalid_federation"
) if !$file;
return $file;
}
sub check_token {
my ($self, %args) = @_;
my $secret = $args{token};
my $token = AccountManager::Token->new(
db => $self->{db},
secret => $secret
);
$self->abort(
log => "No such authentication token $secret",
user => "wrong_token"
) if !$token->load(speculative => 1);
$self->abort(
log => "Authentication token $secret cannot be used for SP $args{entityid}",
user => "wrong_token_for_sp"
) if $token->sp_entityid() ne $args{entityid};
## delete the token
unless ($token->delete()) {
$self->{logger}->errorf(
"Failed to delete authentication token %s",
$secret
);
}
}