lemonldap-ng/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Conf/Tests.pm
Maxime Besson 01724a95d0 Use specified port when testing SMTP conf
When saving the configuration in the manager, the SMTP test would fail
if your SMTP server is not on the standard port 25

The test now takes the port into account
2019-02-03 14:32:55 +01:00

637 lines
21 KiB
Perl

package Lemonldap::NG::Manager::Conf::Tests;
use utf8;
use Lemonldap::NG::Common::Regexp;
our $VERSION = '2.0.2';
## @method hashref tests(hashref conf)
# Return a hash ref where keys are the names of the tests and values
# subroutines to execute.
#
# Subroutines can return one of the followings :
# - (1) : everything is OK
# - (1,message) : OK with a warning
# - (0,message) : NOK
# - (-1,message) : OK, but must be confirmed (ignored if confirm parameter is
# set
#
# Those subroutines can also modify configuration.
#
# @param $conf Configuration to test
# @return hash ref where keys are the names of the tests and values
sub tests {
my $conf = shift;
return {
# 1. CHECKS
# Check if portal is in domain
portalIsInDomain => sub {
return (
1,
(
index( $conf->{portal}, $conf->{domain} ) > 0
? ''
: "Portal seems not to be in the domain $conf->{domain}"
)
);
},
# Check if portal URL is well formated
portalURL => sub {
# Checking for ending slash
$conf->{portal} .= '/'
unless ( $conf->{portal} =~ qr#/$# );
# Deleting trailing ending slash
my $regex = qr#/+$#;
$conf->{portal} =~ s/$regex/\//;
return 1;
},
# Check if virtual hosts are in the domain
vhostInDomainOrCDA => sub {
return 1 if ( $conf->{cda} );
my @pb;
foreach my $vh ( keys %{ $conf->{locationRules} } ) {
push @pb, $vh unless ( index( $vh, $conf->{domain} ) >= 0 );
}
return (
1,
(
@pb
? 'Virtual hosts '
. join( ', ', @pb )
. " are not in $conf->{domain} and cross-domain-authentication is not set"
: undef
)
);
},
# Check if virtual host do not contain a port
vhostWithPort => sub {
my @pb;
foreach my $vh ( keys %{ $conf->{locationRules} } ) {
push @pb, $vh if ( $vh =~ /:/ );
}
if (@pb) {
return ( 0,
'Virtual hosts '
. join( ', ', @pb )
. " contain a port, this is not allowed" );
}
else { return 1; }
},
# Force vhost to be lowercase
vhostUpperCase => sub {
my @pb;
foreach my $vh ( keys %{ $conf->{locationRules} } ) {
push @pb, $vh if ( $vh ne lc $vh );
}
if (@pb) {
return ( 0,
'Virtual hosts '
. join( ', ', @pb )
. " must be in lower case" );
}
else { return 1; }
},
# Check if "userDB" and "authentication" are consistent
authAndUserDBConsistency => sub {
foreach
my $type (qw(Facebook Google OpenID OpenIDConnect SAML WebID))
{
return ( 0,
"\"$type\" can not be used as user database without using \"$type\" for authentication"
)
if ( $conf->{userDB} =~ /$type/
and $conf->{authentication} !~ /$type/ );
}
return 1;
},
# Check that OpenID macros exists
checkAttrAndMacros => sub {
my @tmp;
foreach my $k ( keys %$conf ) {
if ( $k =~
/^(?:openIdSreg_(?:(?:(?:full|nick)nam|languag|postcod|timezon)e|country|gender|email|dob)|whatToTrace)$/
)
{
my $v = $conf->{$k};
$v =~ s/^$//;
next if ( $v =~ /^_/ );
push @tmp,
$k
unless (
defined(
$conf->{exportedVars}->{$v}
or defined( $conf->{macros}->{$v} )
)
);
}
}
return (
1,
(
@tmp
? 'Values of parameter(s) "'
. join( ', ', @tmp )
. '" are not defined in exported attributes or macros'
: ''
)
);
},
# Test that variables are exported if Google is used as UserDB
checkUserDBGoogleAXParams => sub {
my @tmp;
if ( $conf->{userDB} =~ /^Google$/ ) {
foreach my $k ( keys %{ $conf->{exportedVars} } ) {
my $v = $conf->{exportedVars}->{$k};
if ( $v !~ Lemonldap::NG::Common::Regexp::GOOGLEAXATTR() ) {
push @tmp, $v;
}
}
}
return (
1,
(
@tmp
? 'Values of parameter(s) "'
. join( ', ', @tmp )
. '" are not exported by Google'
: ''
)
);
},
# Test that variables are exported if OpenID is used as UserDB
checkUserDBOpenIDParams => sub {
my @tmp;
if ( $conf->{userDB} =~ /^OpenID$/ ) {
foreach my $k ( keys %{ $conf->{exportedVars} } ) {
my $v = $conf->{exportedVars}->{$k};
if ( $v !~ Lemonldap::NG::Common::Regexp::OPENIDSREGATTR() )
{
push @tmp, $v;
}
}
}
return (
1,
(
@tmp
? 'Values of parameter(s) "'
. join( ', ', @tmp )
. '" are not exported by OpenID SREG'
: ''
)
);
},
# Try to use Apache::Session module
testApacheSession => sub {
my ( $id, %h );
my $gc = Lemonldap::NG::Handler::Main->tsv->{sessionStorageModule};
return 1
if ( ( $gc and $gc eq $conf->{globalStorage} )
or $conf->{globalStorage} =~
/^Lemonldap::NG::Common::Apache::Session::/ );
eval "use $conf->{globalStorage}";
return ( -1, "Unknown package $conf->{globalStorage}" ) if ($@);
eval {
tie %h, 'Lemonldap::NG::Common::Apache::Session', undef,
{
%{ $conf->{globalStorageOptions} },
backend => $conf->{globalStorage}
};
};
return ( -1, "Unable to create a session ($@)" )
if ( $@ or not tied(%h) );
eval {
$h{a} = 1;
$id = $h{_session_id} or return ( -1, 'No _session_id' );
untie(%h);
tie %h, 'Lemonldap::NG::Common::Apache::Session', $id,
{
%{ $conf->{globalStorageOptions} },
backend => $conf->{globalStorage}
};
};
return ( -1, "Unable to insert data ($@)" ) if ($@);
return ( -1, "Unable to recover data stored" )
unless ( $h{a} == 1 );
eval { tied(%h)->delete; };
return ( -1, "Unable to delete session ($@)" ) if ($@);
return ( -1,
'All sessions may be lost and you must restart all your Apache servers'
) if ( $gc and $conf->{globalStorage} ne $gc );
return 1;
},
# Warn if cookie name has changed
cookieNameChanged => sub {
my $cn = Lemonldap::NG::Handler::Main->tsv->{cookieName};
return (
1,
(
$cn
and $cn ne $conf->{cookieName}
? 'Cookie name has changed, you must restart all your web servers'
: ()
)
);
},
# Warn if cookie TTL is equal or lower than one hour
cookieTTL => sub {
return 1 unless ( defined $conf->{cookieExpiration} );
return ( 0, "Cookie TTL must be higher than one minute" )
unless ( $conf->{cookieExpiration} == 0
|| $conf->{cookieExpiration} > 60 );
return ( 1, "Cookie TTL should be higher or equal than one hour" )
unless ( $conf->{cookieExpiration} >= 3600
|| $conf->{cookieExpiration} == 0 );
# Return
return 1;
},
# Warn if session timeout is lower than 10 minutes
sessionTimeout => sub {
return 1 unless ( defined $conf->{timeout} );
return ( -1, "Session timeout should be higher than ten minutes" )
unless ( $conf->{timeout} > 600
|| $conf->{timeout} == 0 );
# Return
return 1;
},
# Error if session Activity Timeout is equal or lower than one minute
sessionTimeoutActivity => sub {
return 1 unless ( defined $conf->{timeoutActivity} );
return ( 0,
"Session activity timeout must be higher or equal than one minute"
)
unless ( $conf->{timeoutActivity} > 59
|| $conf->{timeoutActivity} == 0 );
# Return
return 1;
},
# Error if session Activity Timeout is equal or lower than one minute
timeoutActivityInterval => sub {
return 1 unless ( defined $conf->{timeoutActivityInterval} );
return ( 0,
"Activity timeout interval must be lower than session activity timeout"
)
if ( $conf->{timeoutActivity}
and $conf->{timeoutActivity} <=
$conf->{timeoutActivityInterval} );
# Return
return 1;
},
# Warn if manager seems to be unprotected
managerProtection => sub {
return (
1,
(
$conf->{cfgAuthor} eq 'anonymous'
? 'Your manager seems to be unprotected'
: ''
)
);
},
# Test SMTP connection and authentication (warning only)
smtpConnectionAuthentication => sub {
# Skip test if no SMTP configuration
return 1 unless ( $conf->{SMTPServer} );
# Use SMTP
eval "use Net::SMTP";
return ( 1, "Net::SMTP module is required to use SMTP server" )
if ($@);
# Create SMTP object
my $smtp = Net::SMTP->new(
$conf->{SMTPServer},
Timeout => 5,
( $conf->{SMTPPort} ? ( Port => $conf->{SMTPPort} ) : () ),
);
return ( 1,
"SMTP connection to " . $conf->{SMTPServer} . " failed" )
unless ($smtp);
# Skip other tests if no authentication
return 1
unless ( $conf->{SMTPAuthUser} and $conf->{SMTPAuthPass} );
# Try authentication
return ( 1, "SMTP authentication failed" )
unless $smtp->auth( $conf->{SMTPAuthUser},
$conf->{SMTPAuthPass} );
# Return
return 1;
},
# SAML entity ID must be uniq
samlIDPEntityIdUniqueness => sub {
return 1
unless ( $conf->{samlIDPMetaDataXML}
and %{ $conf->{samlIDPMetaDataXML} } );
my @msg;
my $res = 1;
my %entityIds;
foreach my $idpId ( keys %{ $conf->{samlIDPMetaDataXML} } ) {
unless (
$conf->{samlIDPMetaDataXML}->{$idpId}->{samlIDPMetaDataXML}
=~ /entityID=(['"])(.+?)\1/si )
{
push @msg, "$idpId SAML metadata has no EntityID";
$res = 0;
next;
}
my $eid = $2;
if ( defined $entityIds{$eid} ) {
push @msg,
"$idpId and $entityIds{$eid} have the same SAML EntityID";
$res = 0;
next;
}
$entityIds{$eid} = $idpId;
}
return ( $res, join( ', ', @msg ) );
},
samlSPEntityIdUniqueness => sub {
return 1
unless ( $conf->{samlSPMetaDataXML}
and %{ $conf->{samlSPMetaDataXML} } );
my @msg;
my $res = 1;
my %entityIds;
foreach my $spId ( keys %{ $conf->{samlSPMetaDataXML} } ) {
unless (
$conf->{samlSPMetaDataXML}->{$spId}->{samlSPMetaDataXML} =~
/entityID=(['"])(.+?)\1/si )
{
push @msg, "$spId SAML metadata has no EntityID";
$res = 0;
next;
}
my $eid = $2;
if ( defined $entityIds{$eid} ) {
push @msg,
"$spId and $entityIds{$eid} have the same SAML EntityID";
$res = 0;
next;
}
$entityIds{$eid} = $spId;
}
return ( $res, join( ', ', @msg ) );
},
# Try to parse combination with declared modules
checkCombinations => sub {
return 1 unless ( $conf->{authentication} eq 'Combination' );
require Lemonldap::NG::Common::Combination::Parser;
return ( 0, 'No module declared for combination' )
unless ( $conf->{combModules} and %{ $conf->{combModules} } );
my $moduleList;
foreach my $md ( keys %{ $conf->{combModules} } ) {
my $entry = $conf->{combModules}->{$md};
$moduleList->{$md} = (
$entry->{for} == 2 ? [ undef, {} ]
: $entry->{for} == 1 ? [ {}, undef ]
: [ {}, {} ]
);
}
eval {
Lemonldap::NG::Common::Combination::Parser->parse( $moduleList,
$conf->{combination} );
};
return ( 0, $@ ) if ($@);
# Return
return 1;
},
# Check Combination parameters
combinationParameters => sub {
return 1 unless ( $conf->{authentication} eq "Combination" );
return ( 0, "Combination rule must be defined" )
unless ( $conf->{combination} );
return ( 0, 'userDB must be set to "Same" to enable Combination' )
unless ( $conf->{userDB} eq "Same" );
# Return
return 1;
},
# Warn if 2F dependencies seem missing
sfaDependencies => sub {
my $ok = 0;
foreach (qw(u totp utotp yubikey)) {
$ok ||= $conf->{ $_ . '2fActivation' };
last if ($ok);
}
return 1 unless ($ok);
# Use TOTP
if ( $conf->{totp2fActivation}
or $conf->{utotp2fActivation} )
{
eval "use Convert::Base32";
return ( 1,
"Convert::Base32 module is required to enable TOTP" )
if ($@);
}
# Use U2F
if ( $conf->{u2fActivation}
or $conf->{utotp2fActivation} )
{
eval "use Crypt::U2F::Server::Simple";
return ( 1,
"Crypt::U2F::Server::Simple module is required to enable U2F"
) if ($@);
}
# Use Yubikey
if ( $conf->{yubikey2fActivation} ) {
eval "use Auth::Yubikey_WebClient";
return ( 1,
"Auth::Yubikey_WebClient module is required to enable Yubikey"
) if ($@);
}
# Return
return 1;
},
# Warn if TOTP or U2F is enabled with UTOTP (U2F + TOTP)
utotp => sub {
return 1 unless ( $conf->{utotp2fActivation} );
my $w = "";
foreach ( 'totp', 'u' ) {
$w .= uc($_) . "2F is activated twice \n"
if ( $conf->{ $_ . '2fActivation' } eq '1' );
}
return ( 1, ( $w ? $w : () ) );
},
# Warn if TOTP not 6 or 8 digits long
totp2fDigits => sub {
return 1 unless ( $conf->{totp2fActivation} );
return 1 unless ( defined $conf->{totp2fDigits} );
return (
1,
(
(
$conf->{totp2fDigits} == 6
or $conf->{totp2fDigits} == 8
)
? ''
: 'TOTP should be 6 or 8 digits long'
)
);
},
# Test TOTP params
totp2fParams => sub {
return 1 unless ( $conf->{totp2fActivation} );
return ( 0, 'TOTP range must be defined' )
unless ( $conf->{totp2fRange} );
return ( 1, "TOTP interval should be higher than 10s" )
unless ( $conf->{totp2fInterval} > 10 );
# Return
return 1;
},
# Error if Yubikey client ID and secret key are missing
# Warn if Yubikey public ID size is not 12 digits long
yubikey2fParams => sub {
return 1 unless ( $conf->{yubikey2fActivation} );
return ( 0, "Yubikey client ID and secret key must be set" )
unless ( defined $conf->{yubikey2fSecretKey}
&& defined $conf->{yubikey2fClientID} );
return (
1,
(
( $conf->{yubikey2fPublicIDSize} == 12 )
? ''
: 'Yubikey public ID size should be 12 digits long'
)
);
},
# Error if REST 2F verify URL is missing
rest2fVerifyUrl => sub {
return 1 unless ( $conf->{rest2fActivation} );
return ( 0, "REST 2F Verify URL must be set" )
unless ( defined $conf->{rest2fVerifyUrl} );
# Return
return 1;
},
# Warn if 2FA is required without a registrable 2F module enabled
required2FA => sub {
return 1 unless ( $conf->{sfRequired} );
my $msg = '';
my $ok = 0;
foreach (qw(u totp yubikey)) {
$ok ||= $conf->{ $_ . '2fActivation' }
&& $conf->{ $_ . '2fSelfRegistration' };
last if ($ok);
}
$ok ||= $conf->{'utotp2fActivation'}
&& ( $conf->{'u2fSelfRegistration'}
|| $conf->{'totp2fSelfRegistration'} );
$msg = "A self registrable module should be enabled to require 2FA"
unless ($ok);
return ( 1, $msg );
},
# Error if external 2F Send or Validate command is missing
ext2fCommands => sub {
return 1 unless ( $conf->{ext2fActivation} );
return ( 0, "External 2F Send or Validate command must be set" )
unless ( defined $conf->{ext2FSendCommand}
&& defined $conf->{ext2FValidateCommand} );
# Return
return 1;
},
# Warn if XSRF token TTL is higher than 10s
formTimeout => sub {
return 1 unless ( defined $conf->{formTimeout} );
return ( 0, "XSRF form token TTL must be higher than 30s" )
unless ( $conf->{formTimeout} > 30 );
return ( 1, "XSRF form token TTL should not be higher than 2mn" )
if ( $conf->{formTimeout} > 120 );
# Return
return 1;
},
# Warn if number of password reset retries is null
passwordResetRetries => sub {
return 1 unless ( $conf->{portalDisplayResetPassword} );
return ( 1, "Number of reset password retries should not be null" )
unless ( $conf->{passwordResetAllowedRetries} );
# Return
return 1;
},
# Warn if bruteForceProtection enabled without History
bruteForceProtection => sub {
return 1 unless ( $conf->{bruteForceProtection} );
return ( 1,
'"History" plugin is required to enable "BruteForceProtection" plugin'
) unless ( $conf->{loginHistoryEnabled} );
return ( 1,
'Number of failed logins must be higher than 2 to enable "BruteForceProtection" plugin'
) unless ( $conf->{failedLoginNumber} > 2 );
# Return
return 1;
},
# Warn if Mailrest plugin is enabled without Token or Captcha
checkMailResetSecurity => sub {
return 1 unless ( $conf->{portalDisplayResetPassword} );
return ( -1,
'"passwordMailReset" plugin is enabled without CSRF Token neither Captcha required !!!'
)
unless ( $conf->{requireToken}
or $conf->{captcha_mail_enabled} );
# Return
return 1;
},
};
}
1;