lemonldap-ng/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/DBI.pm
2022-01-22 15:45:27 +01:00

480 lines
13 KiB
Perl

##@file
# DBI common functions
##@class
# DBI common functions
package Lemonldap::NG::Portal::Lib::DBI;
use DBI;
use MIME::Base64;
use strict;
use Mouse;
extends 'Lemonldap::NG::Common::Module';
our $VERSION = '2.0.14';
# PROPERTIES
has table => (
is => 'rw',
lazy => 1,
builder => sub {
my $conf = $_[0]->{conf};
return $conf->{dbiUserTable} || $conf->{dbiAuthTable};
}
);
has pivot => (
is => 'rw',
lazy => 1,
builder => sub {
my $conf = $_[0]->{conf};
return $conf->{userPivot} || $conf->{dbiAuthLoginCol};
}
);
has mailField => (
is => 'rw',
lazy => 1,
builder => sub {
my $conf = $_[0]->{conf};
return
$conf->{dbiMailCol}
|| $conf->{userPivot}
|| $conf->{dbiAuthLoginCol};
}
);
# _dbh object: DB connection object
has _dbh => (
is => 'rw',
lazy => 1,
builder => 'dbh',
);
sub dbh {
my $conf = $_[0]->{conf};
$_[0]->{_dbh} = eval {
DBI->connect_cached(
$conf->{dbiAuthChain}, $conf->{dbiAuthUser},
$conf->{dbiAuthPassword}, { RaiseError => 1 }
);
};
if ($@) {
$_[0]->{p}->logger->error("DBI connection error: $@");
return 0;
}
return $_[0]->{_dbh};
}
# INITIALIZATION
# All DBI modules have just to verify that DBI connection is available
sub init {
my ($self) = @_;
$self->_dbh
or $self->logger->error("DBI connection has failed, but let's continue");
unless ( $self->table ) {
$self->logger->error(
"SQL Table name is not set, can't load " . ref($self) );
return 0;
}
return 1;
}
# RUNNING METHODS
# Return hashed password for use in SQL statement
# @param password clear password
# @param hash hash mechanism
# @return SQL statement string
sub hash_password {
my ( $self, $password, $hash ) = @_;
if ($hash) {
$self->logger->debug( "Using " . uc($hash) . " to hash password" );
return uc($hash) . "($password)";
}
else {
$self->logger->debug("No password hash, using clear text for password");
return $password;
}
}
# Return hashed password for use in SQL SELECT statement
# Call hash_password unless encrypt hash is choosen
# @param password clear password
# @param hash hash mechanism
# @return SQL statement string
sub hash_password_for_select {
my ( $self, $password, $hash ) = @_;
my $passwordCol = $self->conf->{dbiAuthPasswordCol};
if ( $hash =~ /^encrypt$/i ) {
return uc($hash) . "($password,$passwordCol)";
}
else {
return $self->hash_password( $password, $hash );
}
}
## @method protected Lemonldap::NG::Portal::_DBI get_password(ref dbh, string user)
# Get password from database
# @param dbh database handler
# @param user user
# @return password
sub get_password {
my $self = shift;
my $dbh = shift;
my $user = shift || $self->{user};
my $table = $self->conf->{dbiAuthTable};
my $loginCol = $self->conf->{dbiAuthLoginCol};
my $passwordCol = $self->conf->{dbiAuthPasswordCol};
my @rows = ();
eval {
my $sth =
$dbh->prepare("SELECT $passwordCol FROM $table WHERE $loginCol=?");
$sth->execute($user);
@rows = $sth->fetchrow_array();
};
if ($@) {
$self->logger->error("DBI error while getting password: $@");
return "";
}
if ( @rows == 1 ) {
$self->logger->debug("Successfully got password from database");
return $rows[0];
}
else {
$self->userLogger->warn("Unable to check password for $user");
return "";
}
}
## @method protected Lemonldap::NG::Portal::_DBI hash_password_from_database
## (ref dbh, string dbmethod, string dbsalt, string password)
# Hash the given password calling the dbmethod function in database
# @param dbh database handler
# @param dbmethod the database method for hashing
# @param salt the salt used for hashing
# @param password the password to hash
# @return hashed password
sub hash_password_from_database {
# Remark: database function must get hexadecimal input
# and send back hexadecimal output
my $self = shift;
my $dbh = shift;
my $dbmethod = shift;
my $dbsalt = shift;
my $password = shift;
# convert password to hexa
my $passwordh = unpack "H*", $password;
my @rows = ();
eval {
my $sth = $dbh->prepare("SELECT $dbmethod('$passwordh$dbsalt')");
$sth->execute();
@rows = $sth->fetchrow_array();
};
if ($@) {
$self->logger->error(
"DBI error while hashing with '$dbmethod' hash function: $@");
$self->userLogger->warn("Unable to check password");
return "";
}
if ( @rows == 1 ) {
$self->logger->debug(
"Successfully hashed password with $dbmethod hash function in database"
);
# convert salt to binary
my $dbsaltb = pack 'H*', $dbsalt;
# convert result to binary
my $res = pack 'H*', $rows[0];
return encode_base64( $res . $dbsaltb, '' );
}
else {
$self->userLogger->warn("Unable to check password with '$dbmethod'");
return "";
}
# Return encode_base64(SQL_METHOD(password + salt) + salt)
}
## @method protected Lemonldap::NG::Portal::_DBI get_salt(string dbhash)
# Return salt from salted hash password
# @param dbhash hash password
# @return extracted salt
sub get_salt {
my $self = shift;
my $dbhash = shift;
my $dbsalt;
# get rid of scheme ({sha256})
$dbhash =~ s/^\{[^}]+\}(.*)$/$1/;
# get binary hash
my $decoded = &decode_base64($dbhash);
# get last 8 bytes
$dbsalt = substr $decoded, -8;
# get hexadecimal version of salt
$dbsalt = unpack "H*", $dbsalt;
return $dbsalt;
}
## @method protected Lemonldap::NG::Portal::_DBI gen_salt()
# Generate 8 bytes of hexadecimal random salt
# @return generated salt
sub gen_salt {
my $self = shift;
my $dbsalt;
my @set = ( '0' .. '9', 'A' .. 'F' );
$dbsalt = join '' => map $set[ rand @set ], 1 .. 16;
return $dbsalt;
}
## @method protected Lemonldap::NG::Portal::_DBI dynamic_hash_password(ref dbh,
## string user, string password, string table, string loginCol, string passwordCol)
# Return hashed password for use in SQL statement
# @param dbh database handler
# @param user connected user
# @param password clear password
# @param table authentication table name
# @param loginCol name of the row containing the login
# @param passwordCol name of the row containing the password
# @return hashed password
sub dynamic_hash_password {
my $self = shift;
my $dbh = shift;
my $user = shift;
my $password = shift;
my $table = shift;
my $loginCol = shift;
my $passwordCol = shift;
# Authorized hash schemes and salted hash schemes
my @validSchemes = split / /, $self->conf->{dbiDynamicHashValidSchemes};
my @validSaltedSchemes = split / /,
$self->conf->{dbiDynamicHashValidSaltedSchemes};
my $dbhash; # hash currently stored in database
my $dbscheme; # current hash scheme stored in database
my $dbmethod; # static hash method corresponding to a database function
my $dbsalt; # current salt stored in database
my $hash; # hash to compute from user password
# Search hash from database
$self->logger->debug("Hash scheme is to be found in database");
$dbhash =
$self->get_password( $dbh, $user, $table, $loginCol, $passwordCol );
# Get the scheme
$dbscheme = $dbhash;
$dbscheme =~ s/^\{([^}]+)\}.*/$1/;
$dbscheme = "" if $dbscheme eq $dbhash;
# no hash scheme => assume clear text
if ( $dbscheme eq "" ) {
$self->logger->info("Password has no hash scheme");
return "?";
}
# salted hash scheme
elsif ( grep( /^$dbscheme$/, @validSaltedSchemes ) ) {
$self->logger->info(
"Valid salted hash scheme: $dbscheme found for user $user");
# extract non salted hash scheme
$dbmethod = $dbscheme;
$dbmethod =~ s/^s//i;
# extract the salt
$dbsalt = $self->get_salt($dbhash);
$self->logger->debug("Get salt from password: $dbsalt");
# Hash password with given hash scheme and salt
$hash =
$self->hash_password_from_database( $dbh, $dbmethod, $dbsalt,
$password );
$hash = "{$dbscheme}$hash";
return "'$hash'";
}
# static hash scheme
elsif ( grep( /^$dbscheme$/, @validSchemes ) ) {
$self->logger->info(
"Valid hash scheme: $dbscheme found for user $user");
# Hash given password with given hash scheme and no salt
$hash =
$self->hash_password_from_database( $dbh, $dbscheme, "", $password );
$hash = "{$dbscheme}$hash";
return "'$hash'";
}
# no valid hash scheme
else {
$self->logger->error("No valid hash scheme: $dbscheme for user $user");
$self->userLogger->warn("Unable to check password for $user");
return "";
}
}
## @method protected Lemonldap::NG::Portal::_DBI dynamic_hash_new_password(ref dbh,
## string user, string password)
# Return hashed password for use in SQL statement
# @param dbh database handler
# @param user connected user
# @param password clear password
# @param dbscheme the scheme to use for hashing
# @return hashed password
sub dynamic_hash_new_password {
my $self = shift;
my $dbh = shift;
my $user = shift;
my $password = shift;
my $dbscheme = $self->conf->{dbiDynamicHashNewPasswordScheme} || "";
# Authorized hash schemes and salted hash schemes
my @validSchemes = split / /, $self->conf->{dbiDynamicHashValidSchemes};
my @validSaltedSchemes = split / /,
$self->conf->{dbiDynamicHashValidSaltedSchemes};
my $dbmethod; # static hash method corresponding to a database function
my $dbsalt; # salt to generate for new hashed password
my $hash; # hash to compute from user password
# no hash scheme => assume clear text
if ( $dbscheme eq "" ) {
$self->logger->info(
"No hash scheme selected, storing password in clear text");
return "?";
}
# salted hash scheme
elsif ( grep( /^$dbscheme$/, @validSaltedSchemes ) ) {
$self->logger->info("Selected salted hash scheme: $dbscheme");
# extract non salted hash scheme
$dbmethod = $dbscheme;
$dbmethod =~ s/^s//i;
# generate the salt
$dbsalt = $self->gen_salt();
$self->logger->debug("Generated salt: $dbsalt");
# Hash given password with given hash scheme and salt
$hash =
$self->hash_password_from_database( $dbh, $dbmethod, $dbsalt,
$password );
$hash = "{$dbscheme}$hash";
return "'$hash'";
}
# static hash scheme
elsif ( grep( /^$dbscheme$/, @validSchemes ) ) {
$self->logger->info("Selected hash scheme: $dbscheme");
# Hash given password with given hash scheme and no salt
$hash =
$self->hash_password_from_database( $dbh, $dbscheme, "", $password );
$hash = "{$dbscheme}$hash";
return "'$hash'";
}
# no valid hash scheme
else {
$self->logger->error("No selected hash scheme: $dbscheme is invalid");
$self->userLogger->warn("Unable to store password for $user");
return "";
}
}
# Verify user and password with SQL SELECT
# @param user user
# @param password password
# @return boolean result
sub check_password {
my ( $self, $user, $password ) = @_;
# If $user is an object then it's a Lemonldap::NG::Portal::Main::Request
# object
if ( ref($user) ) {
$password = $user->data->{password};
$user = $user->{user};
}
my $table = $self->conf->{dbiAuthTable};
my $loginCol = $self->conf->{dbiAuthLoginCol};
my $passwordCol = $self->conf->{dbiAuthPasswordCol};
my $dynamicHash = $self->conf->{dbiDynamicHashEnabled} || 0;
my $passwordsql;
if ( $dynamicHash == 1 ) {
# Dynamic password hashes
$passwordsql =
$self->dynamic_hash_password( $self->dbh, $user, $password, $table,
$loginCol, $passwordCol );
}
else {
# Static Password hashes
$passwordsql =
$self->hash_password_for_select( "?",
$self->conf->{dbiAuthPasswordHash} );
}
my @rows = ();
eval {
my $sth = $self->dbh->prepare(
"SELECT $loginCol FROM $table WHERE $loginCol=? AND $passwordCol=$passwordsql"
);
if ( $passwordsql =~ /.*\?.*/ ) {
$sth->execute( $user, $password );
}
else {
$sth->execute($user);
}
@rows = $sth->fetchrow_array();
};
if ($@) {
# If connection isn't available, error is displayed by dbh()
$self->logger->error("DBI error: $@") if ( $self->_dbh );
return 0;
}
if ( @rows == 1 ) {
$self->logger->debug("One row returned by SQL query");
return 1;
}
else {
$self->userLogger->warn("Bad password for $user");
return 0;
}
}
1;