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

convert to Mojolicious app

parent 16762a73
Branches
Tags
No related merge requests found
package AccountManager::App; package AccountManager::App;
use strict; use Mojo::Base qw(Mojolicious);
use warnings;
use CGI::Simple;
use CGI::Simple::Cookie;
use DateTime;
use English qw(-no_match_vars); use English qw(-no_match_vars);
use Log::Any::Adapter; use Mojolicious::Plugin::TemplateToolkit;
use List::MoreUtils qw(any uniq); use Mojolicious::Plugin::ClientIP;
use Template;
use Template::Constants qw(:chomp); use Template::Constants qw(:chomp);
use Syntax::Keyword::Try;
use UNIVERSAL::require; use UNIVERSAL::require;
use AccountManager::Account; use constant {
use AccountManager::Metadata; ACCESSCHECK_VERSION => '1.2.0'
use AccountManager::Entity; };
use AccountManager::Token;
use AccountManager::Tools;
use AccountManager::L10N;
use AccountManager::Template::Plugin::Quote;
# Format de type URL HTTP ou URN sub startup {
my %patterns = (
entityid => qr{
^
(?:
https?://[\w.:/-]+
|
urn:[\w.:-]+
)
$
}x
);
my %actions = (
home => 'req_home',
status => 'req_status',
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',
);
my $version = '1.2.0';
sub new {
my ($pkg, %args) = @_;
my $self = {
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::Simple->new();
my $lang;
my $cookies = CGI::Simple::Cookie->fetch();
if ($self->{cgi}->param('lang')) {
$lang = $self->{cgi}->param('lang');
$self->{lh} = AccountManager::L10N->get_handle($lang);
$self->{logger}->debugf("setting language from parameter: %s", $lang);
} elsif ($cookies->{lang}) {
$lang = $cookies->{lang}->value();
$self->{lh} = AccountManager::L10N->get_handle($lang);
$self->{logger}->debugf("setting language from cookie: %s", $lang);
} elsif ($lang = $ENV{HTTP_ACCEPT_LANGUAGE}) {
$lang = I18N::LangTags::Detect::detect();
if ($lang =~ /^(\w\w)-(\w\w)$/) {
$lang = $1;
}
$self->{lh} = AccountManager::L10N->get_handle($lang);
$self->{logger}->debugf("setting language from HTTP_ACCEPT_LANGUAGE header: %s", $lang);
} else {
$self->{lh} = AccountManager::L10N->get_handle('en');
$self->{logger}->debugf("using default language");
}
$self->{lh}->load_custom_lexicon($args{custom_l10n});
if (!$self->{configuration}->{mailer}) {
$self->{logger}->fatal(
"No mailer defined in configuration, aborting"
);
$self->respond(
template => 'errors.tt2.html',
data => {
errors => [ 'internal' ]
}
);
}
if (!$self->{configuration}->{idp}) {
$self->{logger}->fatal(
"No IDP 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(
template => 'errors.tt2.html',
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,
);
}
$self->{db} = AccountManager::DB->new();
return $self;
}
sub run {
my ($self) = @_;
if ($self->{logger}->is_debug()) {
Data::Dump->require();
my %parameters = $self->{cgi}->Vars();
$self->{logger}->debugf("input parameters: %s", Data::Dump::dump(\%parameters));
}
# process requested action
my $action = $self->{cgi}->param('action') || 'home';
if ($actions{$action}) {
$self->{logger}->debug("Processing action '$action'");
my $method = $actions{$action};
$self->$method();
} else {
$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},
login_url => $self->{configuration}->{app}->{login_url},
name => $self->{configuration}->{app}->{name},
support_email => $self->{configuration}->{app}->{support_email},
version => $version,
};
$in{data}->{lh} = $self->{lh};
my $templates_dir = $self->{configuration}->{setup}->{templates_dir};
my $templates_theme = $self->{configuration}->{setup}->{templates_theme} || 'default';
my $tt2 = Template->new({
ENCODING => 'utf8',
PRE_CHOMP => CHOMP_ONE,
INCLUDE_PATH => [
sprintf("%s/web/%s", $templates_dir, $templates_theme),
sprintf("%s/web", $templates_dir),
sprintf("%s/accounts", $templates_dir)
]
});
$self->{logger}->debug("Responding with template '$in{template}'");
binmode(STDOUT, ":encoding(UTF-8)");
my $cookie = CGI::Simple::Cookie->new(
-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());
}
exit 0;
}
sub abort {
my $self = shift; 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_select_sp {
my ($self, %args) = @_;
$self->check_authentication(action => 'select_sp')
if $self->{configuration}->{app}->{login_url};
my $sps = AccountManager::Entity->get_entities(
db => $self->{db},
query => [
type => 'sp',
],
sort_by => 'display_name'
);
my $idp;
if ($ENV{HTTP_SHIB_IDENTITY_PROVIDER}) {
my $idps = AccountManager::Entity->get_entities(
db => $self->{db},
query => [
type => 'idp',
entityid => $ENV{HTTP_SHIB_IDENTITY_PROVIDER},
]
);
$idp = $idps->[0];
}
$self->respond(
template => 'select_sp.tt2.html',
data => {
action => 'select_sp',
sps => $sps,
idp => $idp,
}
);
}
sub req_select_email {
my ($self, %args) = @_;
$self->check_authentication(action => 'select_email')
if $self->{configuration}->{app}->{login_url};
my $entityid = $self->get_parameter(name => 'entityid');
my $sp = AccountManager::Entity->new(
db => $self->{db},
entityid => $entityid
);
$self->abort(
log => sprintf("No such SP '%s' in database", $entityid),
user => "no_such_entity"
) if !$sp->load(speculative => 1);
# override metadata contacts if needed
my $contacts =
$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
$sp->contacts(split(/, */, $contacts));
}
}
$self->respond( $self->plugin('INIConfig', { file => $ENV{MOJO_CONFIG} || 'conf/manager.conf' });
template => 'select_email.tt2.html',
data => { $self->plugin(
action => 'select_email', 'TemplateToolkit',
sp => $sp, {
entityid => $entityid, name => 'tt2',
template => {
ABSOLUTE => 1,
ENCODING => 'utf8',
PRE_CHOMP => CHOMP_ONE,
PLUGIN_BASE => 'AccountManager::Template::Plugin',
}
} }
); );
}
sub req_complete_challenge {
my ($self, %args) = @_;
$self->check_authentication(action => 'complete_challenge')
if $self->{configuration}->{app}->{login_url};
my $entityid = $self->get_parameter(name => 'entityid');
my $email = $self->get_parameter(name => 'email');
my $sp = AccountManager::Entity->new(
db => $self->{db},
entityid => $entityid,
);
$self->abort(
log => sprintf("No such SP '%s' in database", $entityid),
user => "no_such_entity"
) if !$sp->load(speculative => 1);
# override metadata contacts if needed
my $contacts =
$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
$sp->contacts(split(/, */, $contacts));
}
}
## Check that email is a known contact for this SP
$self->abort(
log => "Requested a token for SP $entityid with unautorized address $email",
user => "internal",
) if !$sp->is_contact($email);
# delete any previous token for the same email/service couple $self->plugin('ClientIP');
my $old_token = AccountManager::Token->new(
db => $self->{db},
email_address => $email,
entityid => $entityid,
);
if ($old_token->load(speculative => 1)) { my $config = $self->config();
$self->abort(
log => "Failed to delete old authentication token",
user => "internal"
) if !$old_token->delete();
}
# compute a new token $self->log(
my $validity_period = Mojo::Log->new(
$self->{configuration}->{service}->{tokens_validity_period}; path => $config->{logger}->{file},
my $token = AccountManager::Token->new( level => ($config->{logger}->{level} || 'trace')
db => $self->{db}, )
email_address => $email,
entityid => $entityid,
creation_date => DateTime->now(),
expiration_date => DateTime->now()->add(hours => $validity_period),
secret => AccountManager::Tools::generate_secret(20)
); );
$self->abort( my $theme = $config->{setup}->{templates_theme} || 'default';
log => "Failed to save creation authentication token", my $base_templates_dir = $self->home()->child('templates');
user => "internal"
) if !$token->save();
my $theme_templates_dir = sprintf( my $renderer = $self->renderer();
"%s/mail/%s", $renderer->default_handler('tt2');
$self->{configuration}->{setup}->{templates_dir}, $renderer->paths([
$self->{configuration}->{setup}->{templates_theme} || 'default' $base_templates_dir->child('web', $theme),
); $base_templates_dir->child('web'),
my $default_templates_dir = sprintf( $base_templates_dir->child('accounts'),
"%s/mail", ]);
$self->{configuration}->{setup}->{templates_dir},
);
my $templates_dir = -d $theme_templates_dir ?
$theme_templates_dir :
$default_templates_dir;
# build content $self->defaults(
my $tt2 = Template->new({
ENCODING => 'utf8',
PRE_CHOMP => CHOMP_ONE,
INCLUDE_PATH => $templates_dir
});
my $source_ip = get_source_address();
my $user =
$ENV{'HTTP_DISPLAYNAME'} ? $ENV{'HTTP_DISPLAYNAME'} :
$ENV{'displayName'} ? $ENV{'displayName'} :
undef;
my $idp =
$ENV{'HTTP_SHIB_IDENTITY_PROVIDER'} ? $ENV{'HTTP_SHIB_IDENTITY_PROVIDER'} :
$ENV{'Shib-Identity-Provider'} ? $ENV{'Shib-Identity-Provider'} :
undef;
my $data = {
app => { app => {
url => $self->{configuration}->{app}->{url}, support_url => $config->{app}->{support_url},
support_email => $self->{configuration}->{app}->{support_email}, support_email => $config->{app}->{support_email},
version => $self->{configuration}->{app}->{version}, login_url => $config->{app}->{login_url},
name => $self->{configuration}->{app}->{name}, logout_url => $config->{app}->{logout_url},
}, name => $config->{app}->{name},
user => $user, version => ACCESSCHECK_VERSION
source_ip => $source_ip,
idp => {
entityid => $idp,
}, },
sp => {
entityid => $entityid,
},
to => $email,
token => $token->secret(),
challenge_url => sprintf(
'%s?action=complete_challenge&entityid=%s&email=%s',
$self->{configuration}->{app}->{url},
$entityid,
$email,
),
lh => $self->{lh},
};
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);
# wrap in message
Email::MIME->require();
Email::Sender::Simple->require();
my $message = Email::MIME->create(
header_str => [
'From' => sprintf(
'%s <%s>',
$self->{configuration}->{app}->{name},
$self->{configuration}->{mailer}->{from}
),
'To' => $email,
'Subject' => sprintf(
'[%s] %s',
$self->{configuration}->{app}->{name},
$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
),
]
);
eval {
local $ENV{PATH} = '/bin:/sbin:/usr/bin:/usr/sbin';
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",
$email,
$entityid,
$token->secret(),
);
my @profiles =
map { m/([^\/]+).tt2$/ }
glob($self->{configuration}->{setup}->{templates_dir} . '/accounts/*.tt2' );
$self->respond(
template => 'complete_challenge.tt2.html',
data => {
action => 'complete_challenge',
entityid => $entityid,
email => $email,
validity => $self->{configuration}->{service}->{account_validity_period},
profiles => \@profiles
}
);
}
sub req_create_accounts {
my ($self, %args) = @_;
$self->check_authentication(action => 'create_accounts')
if $self->{configuration}->{app}->{login_url};
my $entityid = $self->get_parameter(name => 'entityid');
my $token = $self->get_parameter(name => 'token');
my $email = $self->get_parameter(name => 'email');
my $validity = $self->get_parameter(name => 'validity');
my @profiles = $self->get_multivalued_parameter(name => 'profiles');
$self->check_token(token => $token, entityid => $entityid);
my $sp = AccountManager::Entity->new(
db => $self->{db},
entityid => $entityid,
);
$self->abort(
log => sprintf("No such SP '%s' in database", $entityid),
user => "no_such_entity"
) if !$sp->load(speculative => 1);
## create test accounts
my @accounts;
my $creation_date = DateTime->now();
my $token_expiration_date = DateTime->now()->add(
hours => $self->{configuration}->{service}->{tokens_validity_period}
);
my $account_expiration_date = DateTime->now()->add(
days => $validity
);
my $download_token = AccountManager::Token->new(
db => $self->{db},
email_address => $email,
entityid => $entityid,
creation_date => $creation_date,
expiration_date => $token_expiration_date,
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 (@profiles) {
my $password = AccountManager::Tools::generate_password(10);
my $account = AccountManager::Account->new(
db => $self->{db},
profile => $profile,
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 => $creation_date,
expiration_date => $account_expiration_date,
);
next unless $account->save();
push @accounts, $account;
}
$self->abort(
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});
eval {
AccountManager::Tools::update_ssp_authsources(
$self->{configuration}->{setup}->{templates_dir},
$self->{configuration}->{setup}->{accounts_file},
$accounts
);
};
$self->abort(
log => "Failed to create simpleSAMLphp configuration file: $EVAL_ERROR",
user => "accounts_creation_failure"
) if $EVAL_ERROR;
$self->{logger}->infof(
"Token validated for entityid=%s",
$entityid,
);
my $download_url = sprintf(
"%s?action=download_accounts&entityid=%s&token=%s&key=%s",
$self->{configuration}->{app}->{url},
$entityid,
$download_token->secret(),
$key
);
$self->respond(
template => 'create_accounts.tt2.html',
data => {
action => 'create_accounts',
accounts => \@accounts,
idp => {
name => $self->{configuration}->{idp}->{name},
},
sp => {
entityid => $entityid,
url => $sp->information_url(),
},
email => $email,
download_url => $download_url,
days => $validity,
}
);
}
sub req_download_accounts {
my ($self) = @_;
$self->check_authentication(action => 'download_accounts')
if $self->{configuration}->{app}->{login_url};
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 => [
token => $token
],
);
foreach my $account (@$accounts) {
my $password = AccountManager::Tools::decrypt(
$account->password_crypt(), $key
);
$account->password($password);
}
binmode(STDOUT, ":encoding(UTF-8)");
print $self->{cgi}->header(
-type => 'text/csv',
-content_disposition => 'attachment; filename="accounts.csv"'
); );
my $templates_dir = $self->{configuration}->{setup}->{templates_dir};
my $tt2 = Template->new({ my $routes = $self->routes();
ENCODING => 'utf8',
PRE_CHOMP => CHOMP_ONE,
INCLUDE_PATH => [
sprintf("%s/other", $templates_dir),
sprintf("%s/accounts", $templates_dir),
],
});
unless ($tt2->process("accounts.csv.tt2", { accounts => $accounts }, \*STDOUT)) { $routes->get('/')->to(controller => 'home', action => 'run')->name('home');
printf "Content-type: text/plain\n\n Error: %s", $tt2->error(); $routes->get('/status')->to(controller => 'status', action => 'run')->name('status');
$self->{logger}->errorf("Web parser error : %s", $tt2->error()); $routes->get('/step1')->to(controller => 'step1', action => 'run')->name('step1');
} $routes->get('/step2')->to(controller => 'step2', action => 'run')->name('step2');
} $routes->get('/step3')->to(controller => 'step3', action => 'run')->name('step3');
$routes->get('/step4')->to(controller => 'step4', action => 'run')->name('step4');
$routes->get('/step5')->to(controller => 'step5', action => 'run')->name('step5');
## Return the homepage of the service $self->helper(
sub req_home { string_to_list => sub {
my ($self) = @_; my $self = shift;
my $value = shift;
$self->respond( return defined $value ? split(/, */, $value) : ();
template => 'home.tt2.html',
data => {
action => 'home'
} }
); );
}
sub req_status {
my ($self) = @_;
Net::IP->require();
my $source_ip_string = get_source_address();
my $source_ip = Net::IP->new($source_ip_string);
my @allowed_ips_strings = $self->{configuration}->{status}->{allowed} ?
split(/, */, $self->{configuration}->{status}->{allowed}) : ();
my @allowed_ips = map { Net::IP->new($_) } @allowed_ips_strings;
if (any { $_->overlaps($source_ip) } @allowed_ips) {
Sys::Hostname->require();
JSON->require();
print $self->{cgi}->header(
-type => 'application/json',
-charset => 'utf8',
);
my $status = $self->{configuration}->{status}->{disabled} ? 'disabled' : 'available';
print JSON->new()->encode({
status => $status,
host => Sys::Hostname::hostname()
});
} else {
$self->{logger}->errorf("Unauthorized access from %s", $source_ip_string);
print $self->{cgi}->header(
-status => '403 unauthorized',
-type => 'text/plain',
);
print "Unauthorized access";
}
exit 0;
}
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"
) if $value !~ $patterns{$name};
}
return $value;
}
sub get_multivalued_parameter {
my ($self, %args) = @_;
my $name = $args{name};
my @values = $self->{cgi}->param($name);
$self->abort(
log => "Missing parameter: $name",
user => "missing_$name"
) if !@values;
if ($patterns{$name}) {
$self->abort(
log => "Incorrect parameter format: $name",
user => "format_$name"
) if any { $_ !~ $patterns{$name} } @values;
}
return @values;
}
sub get_metadata_file {
my ($self, %args) = @_;
my $federation = $args{federation};
my $file = $self->{configuration}->{$federation}->{metadata};
$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->entityid() ne $args{entityid};
## delete the token
unless ($token->delete()) {
$self->{logger}->errorf(
"Failed to delete authentication token %s",
$secret
);
}
}
sub check_authentication {
my $self = shift;
my %args = @_;
$self->abort(
log => "unauthenticated user for action $args{action}",
user => "unauthenticated"
) if !$ENV{HTTP_SHIB_IDENTITY_PROVIDER};
$self->abort(
log => "no displayName attribute for identity provider $ENV{HTTP_SHIB_IDENTITY_PROVIDER}",
user => "no_displayname_attribute"
) if !$ENV{'HTTP_DISPLAYNAME'};
}
sub get_source_address {
return $ENV{HTTP_X_FORWARDED_FOR} ?
(split(/, /, $ENV{HTTP_X_FORWARDED_FOR}))[0] :
$ENV{REMOTE_ADDR};
} }
1; 1;
package AccountManager::App::Controller;
use Mojo::Base qw(Mojolicious::Controller);
use English qw(-no_match_vars);
use Syntax::Keyword::Try;
use AccountManager::DB;
use AccountManager::L10N;
use AccountManager::Token;
sub init_l10n {
my $self = shift;
my $log = $self->app()->log();
# lang identification first, as needed for any further error message
my ($l10n, $lang);
if ($self->param('lang')) {
$lang = $self->param('lang');
$l10n = AccountManager::L10N->get_handle($lang);
$log->debug(sprintf("setting language from parameter: %s", $lang));
} elsif ($self->session('lang')) {
$lang = $self->session('lang');
$l10n = AccountManager::L10N->get_handle($lang);
$log->debug(sprintf("setting language from session: %s", $lang));
} elsif ($self->req()->headers->header('Accept-Language')) {
$l10n = AccountManager::L10N->get_handle();
$lang = $l10n->language_tag();
$log->debug(sprintf("setting language from Accept-Language header: %s", $lang));
} else {
$lang = 'en';
$l10n = AccountManager::L10N->get_handle($lang);
}
$self->session(lang => $lang);
$self->stash(lang => $lang);
$self->stash(l10n => $l10n);
return $l10n;
}
sub init_db {
my $self = shift;
my $config = $self->app()->config();
AccountManager::DB->register_db(
driver => $config->{database}->{type},
database => $config->{database}->{name},
host => $config->{database}->{host},
password => $config->{database}->{password},
username => $config->{database}->{username},
options => [ $self->string_to_list($config->{database}->{options}) ]
);
my $db;
try {
$db = AccountManager::DB->new();
} catch {
}
$self->stash(db => $db);
return $db;
}
sub check_authentication {
my $self = shift;
my $idp =
$ENV{'Shib_Identity_Provider'} || # local SP
$self->req()->headers()->header('Shib-Identity-Provider'); # remote SP
return $self->abort(
status => 401,
log_message => sprintf("unauthenticated user for action %s", $self->current_route()),
user_message => Registry::Error::AuthenticationRequired->new()
) if !$idp;
return 1;
}
sub check_token {
my ($self, %args) = @_;
my $secret = $args{token};
my $token = AccountManager::Token->new(
db => $self->{db},
secret => $secret
);
return $self->abort(
status => 400,
log_message => "No such authentication token $secret",
user_message => "wrong_token"
) if !$token->load(speculative => 1);
return $self->abort(
status => 400,
log_message => "Authentication token $secret cannot be used for SP $args{entityid}",
user_message => "wrong_token_for_sp"
) if $token->entityid() ne $args{entityid};
## delete the token
try {
$token->delete();
} catch {
$self->app()->log()->error(
sprintf("Failed to delete authentication token %s", $secret)
);
}
return 1;
}
sub abort {
my $self = shift;
my %args = @_;
my $status = $args{status} || 200;
my $format = $args{format} || 'html';
my $db = $self->stash('db');
$db->rollback() if $db && $db->in_transaction();
$self->app()->log()->error($args{log_message}) if $args{log_message};
$self->stash(error => $args{user_message});
$self->render(status => $status, template => 'errors', format => 'html');
return;
}
sub loc {
my $self = shift;
return $self->stash('l10n')->maketext(@_);
}
1;
package AccountManager::App::Home;
=head1 NAME
AccountManager::App::Home - Home page controller
=head1 DESCRIPTION
=cut
use Mojo::Base qw(AccountManager::App::Controller);
use English qw(-no_match_vars);
use Syntax::Keyword::Try;
sub run {
my $self = shift;
$self->init_l10n();
$self->render(status => 200, template => 'home', format => 'html');
}
1;
package AccountManager::App::Status;
=head1 NAME
AccountManager::App::Status - Health monitoring controller
=head1 DESCRIPTION
Health monitoring page
Access: restricted by IP address
=cut
use Mojo::Base qw(AccountManager::App::Controller);
use English qw(-no_match_vars);
use List::MoreUtils qw(none);
use Net::IP;
use Sys::Hostname;
use Syntax::Keyword::Try;
=head1 INSTANCE METHODS
=head2 run()
Return the health status of the frontend.
=cut
sub run {
my $self = shift;
my $config = $self->app()->config();
if (!$config->{status}) {
$self->render(
status => 403,
test => "unauthorized access"
);
return;
}
my $client_ip = Net::IP->new($self->client_ip());
my @allowed_ips =
map { Net::IP->new($_) }
$self->string_to_list($config->{status}->{allowed});
if (none { $_->overlaps($client_ip) } @allowed_ips) {
$self->render(
status => 403,
test => "unauthorized access"
);
return;
}
my $status = $config->{status}->{disabled} ? 'disabled' : 'available';
my $health = {
status => $status,
host => hostname(),
};
$self->render(status => 200, json => $health);
}
1;
package AccountManager::App::Step1;
use Mojo::Base qw(AccountManager::App::Controller);
use English qw(-no_match_vars);
use Syntax::Keyword::Try;
use AccountManager::Entity;
sub run {
my $self = shift;
my $config = $self->app()->config();
my $log = $self->app()->log();
$self->init_db();
$self->init_l10n();
if ($config->{app}->{login_url}) {
return if !$self->check_authentication();
}
my $db = $self->stash('db');
my $sps = AccountManager::Entity->get_entities(
db => $db,
query => [
type => 'sp',
],
sort_by => 'display_name'
);
my $idp;
if ($ENV{HTTP_SHIB_IDENTITY_PROVIDER}) {
my $idps = AccountManager::Entity->get_entities(
db => $db,
query => [
type => 'idp',
entityid => $ENV{HTTP_SHIB_IDENTITY_PROVIDER},
]
);
$idp = $idps->[0];
}
$self->stash(sps => $sps);
$self->stash(idp => $idp);
$self->render(
status => 200,
template => 'select_sp',
format => 'html'
);
}
1;
package AccountManager::App::Step2;
use Mojo::Base qw(AccountManager::App::Controller);
use English qw(-no_match_vars);
use Syntax::Keyword::Try;
use AccountManager::Entity;
sub run {
my $self = shift;
my $config = $self->app()->config();
my $log = $self->app()->log();
$self->init_db();
$self->init_l10n();
if ($config->{app}->{login_url}) {
return if !$self->check_authentication();
}
my $entityid = $self->param('entityid');
my $db = $self->stash('db');
my $sp = AccountManager::Entity->new(
db => $db,
entityid => $entityid
);
return $self->abort(
log_message => sprintf("No such SP '%s' in database", $entityid),
user_message => "no_such_entity"
) if !$sp->load(speculative => 1);
# override metadata contacts if needed
my $contacts =
$config->{$entityid}->{contacts} ||
$config->{service}->{contacts};
if ($contacts) {
if ($contacts =~ /^\+(.+)/) {
# complement original contacts
$sp->contacts($sp->contacts(), split(/, */, $1));
} else {
# replace original contacts
$sp->contacts(split(/, */, $contacts));
}
}
$self->stash(sp => $sp);
$self->stash(entityid => $entityid);
$self->render(
status => 200,
template => 'select_email',
format => 'html'
);
}
1;
package AccountManager::App::Step3;
use Mojo::Base qw(AccountManager::App::Controller);
use DateTime;
use Email::MIME;
use Email::Sender::Simple;
use English qw(-no_match_vars);
use Syntax::Keyword::Try;
use Template::Constants qw(:chomp);
use AccountManager::Entity;
use AccountManager::Token;
use AccountManager::Tools;
sub run {
my $self = shift;
my $config = $self->app()->config();
my $log = $self->app()->log();
$self->init_db();
$self->init_l10n();
if ($config->{app}->{login_url}) {
return if !$self->check_authentication();
}
my $entityid = $self->param('entityid');
my $email = $self->param('email');
my $db = $self->stash('db');
my $l10n = $self->stash('l10n');
my $sp = AccountManager::Entity->new(
db => $db,
entityid => $entityid
);
return $self->abort(
log_message => sprintf("No such SP '%s' in database", $entityid),
user_message => "no_such_entity"
) if !$sp->load(speculative => 1);
# override metadata contacts if needed
my $contacts =
$config->{$entityid}->{contacts} ||
$config->{service}->{contacts};
if ($contacts) {
if ($contacts =~ /^\+(.+)/) {
# complement original contacts
$sp->contacts($sp->contacts(), split(/, */, $1));
} else {
# replace original contacts
$sp->contacts(split(/, */, $contacts));
}
}
## Check that email is a known contact for this SP
return $self->abort(
log_message => "Requested a token for SP $entityid with unautorized address $email",
user_message => "internal",
) if !$sp->is_contact($email);
# delete any previous token for the same email/service couple
my $old_token = AccountManager::Token->new(
db => $db,
email_address => $email,
entityid => $entityid,
);
if ($old_token->load(speculative => 1)) {
try {
$old_token->delete();
} catch {
return $self->abort(
log_message => "Failed to delete old authentication token",
user_message => "internal"
);
}
}
# compute a new token
my $validity_period =
$config->{service}->{tokens_validity_period};
my $token = AccountManager::Token->new(
db => $db,
email_address => $email,
entityid => $entityid,
creation_date => DateTime->now(),
expiration_date => DateTime->now()->add(hours => $validity_period),
secret => AccountManager::Tools::generate_secret(20)
);
try {
$token->save();
} catch {
return $self->abort(
log_message => "Failed to save creation authentication token",
user_message => "internal"
);
}
# build content
my $theme = $config->{setup}->{templates_theme} || 'default';
my $base_templates_dir = $self->app()->home()->child('templates');
my $tt2 = Template->new({
ENCODING => 'utf8',
PRE_CHOMP => CHOMP_ONE,
INCLUDE_PATH => [
$base_templates_dir->child('mail', $theme),
$base_templates_dir->child('mail'),
]
});
my $user =
$ENV{'HTTP_DISPLAYNAME'} ? $ENV{'HTTP_DISPLAYNAME'} :
$ENV{'displayName'} ? $ENV{'displayName'} :
undef;
my $idp =
$ENV{'HTTP_SHIB_IDENTITY_PROVIDER'} ? $ENV{'HTTP_SHIB_IDENTITY_PROVIDER'} :
$ENV{'Shib-Identity-Provider'} ? $ENV{'Shib-Identity-Provider'} :
undef;
my $data = {
app => {
url => $config->{app}->{url},
support_email => $config->{app}->{support_email},
version => $config->{app}->{version},
name => $config->{app}->{name},
},
user => $user,
source_ip => $self->client_ip(),
idp => { entityid => $idp, },
sp => { entityid => $entityid, },
to => $email,
token => $token->secret(),
challenge_url => $self->url_for('step3')->query(entityid => $entityid, email => $email)->to_abs(),
lh => $l10n
};
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);
my $message = Email::MIME->create(
header_str => [
'From' => sprintf('%s <%s>', $config->{app}->{name}, $config->{mailer}->{from}),
'To' => $email,
'Subject' => sprintf('[%s] %s', $config->{app}->{name}, $l10n->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
),
]
);
try {
local $ENV{PATH} = '/bin:/sbin:/usr/bin:/usr/sbin';
Email::Sender::Simple->send($message);
} catch($error) {
return $self->abort(
log_message => "Mail notification error: $error",
user_message => "mail_notification_failure"
);
}
$log->info(
sprintf(
"Token send to %s for entityid=%s;token=%s",
$email,
$entityid,
$token->secret(),
)
);
my $profiles = $base_templates_dir
->child('accounts')
->list()
->map(sub { m/([^\/]+).tt2$/})
->to_array();
$self->stash(entityid => $entityid);
$self->stash(email => $email);
$self->stash(validity => $config->{service}->{account_validity_period});
$self->stash(profiles => $profiles);
$self->render(
status => 200,
template => 'complete_challenge',
format => 'html'
);
}
1;
package AccountManager::App::Step4;
use Mojo::Base qw(AccountManager::App::Controller);
use DateTime;
use Email::MIME;
use Email::Sender::Simple;
use English qw(-no_match_vars);
use Syntax::Keyword::Try;
use Template::Constants qw(:chomp);
use AccountManager::Account;
use AccountManager::Entity;
use AccountManager::Token;
use AccountManager::Tools;
sub run {
my $self = shift;
my $config = $self->app()->config();
my $log = $self->app()->log();
$self->init_db();
$self->init_l10n();
if ($config->{app}->{login_url}) {
return if !$self->check_authentication();
}
my $entityid = $self->param('entityid');
my $email = $self->param('email');
my $token = $self->param('token');
my $validity = $self->param('validity');
my $profiles = $self->every_param('profiles');
my $db = $self->stash('db');
my $l10n = $self->stash('l10n');
return if !$self->check_token(token => $token, entityid => $entityid);
my $sp = AccountManager::Entity->new(
db => $db,
entityid => $entityid,
);
return $self->abort(
log_message => sprintf("No such SP '%s' in database", $entityid),
user_message => "no_such_entity"
) if !$sp->load(speculative => 1);
## create test accounts
my @accounts;
my $creation_date = DateTime->now();
my $token_expiration_date = DateTime->now()->add(
hours => $config->{service}->{tokens_validity_period}
);
my $account_expiration_date = DateTime->now()->add(
days => $validity
);
my $download_token = AccountManager::Token->new(
db => $db,
email_address => $email,
entityid => $entityid,
creation_date => $creation_date,
expiration_date => $token_expiration_date,
secret => AccountManager::Tools::generate_secret(20)
);
try {
$download_token->save();
} catch {
return $self->abort(
log_message => "Failed to save download authentication token",
user_message => "internal"
);
}
my $key = AccountManager::Tools::generate_secret(10);
foreach my $profile (@$profiles) {
my $password = AccountManager::Tools::generate_password(10);
my $account = AccountManager::Account->new(
db => $db,
profile => $profile,
entityid => $entityid,
scope => $config->{idp}->{scope},
password => $password,
password_crypt => AccountManager::Tools::encrypt($password, $key),
password_hash => AccountManager::Tools::sha256_hash($password),
token => $download_token->secret(),
creation_date => $creation_date,
expiration_date => $account_expiration_date,
);
next unless $account->save();
push @accounts, $account;
}
return $self->abort(
log_message => "Failed to create test accounts for SP $entityid",
user_message => "accounts_creation_failure"
) if !@accounts;
## Update simpleSAMLphp configuration to enable test accounts
my $accounts = AccountManager::Account->get_accounts(db => $db);
try {
AccountManager::Tools::update_ssp_authsources(
$self->app()->home()->child('templates'),
$config->{setup}->{accounts_file},
$accounts
);
} catch($error) {
return $self->abort(
log_message => "Failed to create simpleSAMLphp configuration file: $error",
user_message => "accounts_creation_failure"
);
}
$log->info(sprintf("Token validated for entityid=%s", $entityid));
$self->stash(accounts => \@accounts);
$self->stash(idp => { name => $config->{idp}->{name} });
$self->stash(sp => { entityid => $entityid, url => $sp->information_url() });
$self->stash(email => $email);
$self->stash(days => $validity);
$self->stash(
download_url => $self->url_for('step5')->query(
entityid => $entityid,
token => $download_token->secret(),
key => $key
)
);
$self->render(
status => 200,
template => 'create_accounts',
format => 'html'
);
}
1;
package AccountManager::App::Step5;
use Mojo::Base qw(AccountManager::App::Controller);
use DateTime;
use Email::MIME;
use Email::Sender::Simple;
use English qw(-no_match_vars);
use Syntax::Keyword::Try;
use Template::Constants qw(:chomp);
use AccountManager::Account;
use AccountManager::Tools;
sub run {
my $self = shift;
my $app = $self->app();
my $config = $app->config();
my $log = $app->log();
$self->init_db();
$self->init_l10n();
if ($config->{app}->{login_url}) {
return if !$self->check_authentication();
}
my $entityid = $self->param('entityid');
my $token = $self->param('token');
my $key = $self->param('key');
my $db = $self->stash('db');
return if !$self->check_token(token => $token, entityid => $entityid);
# load accounts from database
my $accounts = AccountManager::Account->get_accounts(
db => $db,
query => [
token => $token
],
);
foreach my $account (@$accounts) {
my $password = AccountManager::Tools::decrypt(
$account->password_crypt(), $key
);
$account->password($password);
}
$app->types()->type(csv => 'text/csv');
$app->renderer()->paths([
$app->home()->child('templates', 'other'),
$app->home()->child('templates', 'accounts'),
]);
$self->stash(accounts => $accounts);
$self->render(
status => 200,
template => 'accounts',
format => 'csv'
);
}
1;
modulesdir = $(datadir)/access-check/lib applibdir = $(pkgdatadir)/lib
nobase_modules_DATA = \ nobase_applib_DATA = \
AccountManager/Token.pm \ AccountManager/Token.pm \
AccountManager/DB.pm \ AccountManager/DB.pm \
AccountManager/DB/Object.pm \ AccountManager/DB/Object.pm \
...@@ -12,6 +12,14 @@ nobase_modules_DATA = \ ...@@ -12,6 +12,14 @@ nobase_modules_DATA = \
AccountManager/L10N/en.pm \ AccountManager/L10N/en.pm \
AccountManager/L10N/fr.pm \ AccountManager/L10N/fr.pm \
AccountManager/App.pm \ AccountManager/App.pm \
AccountManager/App/Home.pm \
AccountManager/App/Controller.pm \
AccountManager/App/Status.pm \
AccountManager/App/Step1.pm \
AccountManager/App/Step2.pm \
AccountManager/App/Step3.pm \
AccountManager/App/Step4.pm \
AccountManager/App/Step5.pm \
AccountManager/Template/Plugin/Quote.pm AccountManager/Template/Plugin/Quote.pm
EXTRA_DIST = $(nobase_modules_DATA) EXTRA_DIST = $(nobase_applib_DATA)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment