Files
scripts/Perl OTRS/Kernel/System/User.pm
2024-10-14 00:08:40 +02:00

1576 lines
43 KiB
Perl

# --
# Copyright (C) 2001-2019 OTRS AG, https://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --
package Kernel::System::User;
use strict;
use warnings;
use Crypt::PasswdMD5 qw(unix_md5_crypt apache_md5_crypt);
use Digest::SHA;
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::Cache',
'Kernel::System::CheckItem',
'Kernel::System::DB',
'Kernel::System::Encode',
'Kernel::System::Log',
'Kernel::System::Main',
'Kernel::System::SearchProfile',
'Kernel::System::DateTime',
'Kernel::System::Valid',
);
=head1 NAME
Kernel::System::User - user lib
=head1 DESCRIPTION
All user functions. E. g. to add and updated user and other functions.
=head1 PUBLIC INTERFACE
=head2 new()
Don't use the constructor directly, use the ObjectManager instead:
my $UserObject = $Kernel::OM->Get('Kernel::System::User');
=cut
sub new {
my ( $Type, %Param ) = @_;
# allocate new hash for object
my $Self = {};
bless( $Self, $Type );
# get config object
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
# get user table
$Self->{UserTable} = $ConfigObject->Get('DatabaseUserTable') || 'user';
$Self->{UserTableUserID} = $ConfigObject->Get('DatabaseUserTableUserID') || 'id';
$Self->{UserTableUserPW} = $ConfigObject->Get('DatabaseUserTableUserPW') || 'pw';
$Self->{UserTableUser} = $ConfigObject->Get('DatabaseUserTableUser') || 'login';
$Self->{CacheType} = 'User';
$Self->{CacheTTL} = 60 * 60 * 24 * 20;
# set lower if database is case sensitive
$Self->{Lower} = '';
if ( $Kernel::OM->Get('Kernel::System::DB')->GetDatabaseFunction('CaseSensitive') ) {
$Self->{Lower} = 'LOWER';
}
return $Self;
}
=head2 GetUserData()
get user data (UserLogin, UserFirstname, UserLastname, UserEmail, ...)
my %User = $UserObject->GetUserData(
UserID => 123,
);
or
my %User = $UserObject->GetUserData(
User => 'franz',
Valid => 1, # not required -> 0|1 (default 0)
# returns only data if user is valid
NoOutOfOffice => 1, # not required -> 0|1 (default 0)
# returns data without out of office infos
);
=cut
sub GetUserData {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{User} && !$Param{UserID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need User or UserID!',
);
return;
}
# get config object
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
# get configuration for the full name order
my $FirstnameLastNameOrder = $ConfigObject->Get('FirstnameLastnameOrder') || 0;
# check if result is cached
if ( $Param{Valid} ) {
$Param{Valid} = 1;
}
else {
$Param{Valid} = 0;
}
if ( $Param{NoOutOfOffice} ) {
$Param{NoOutOfOffice} = 1;
}
else {
$Param{NoOutOfOffice} = 0;
}
my $CacheKey;
if ( $Param{User} ) {
$CacheKey = join '::', 'GetUserData', 'User',
$Param{User},
$Param{Valid},
$FirstnameLastNameOrder,
$Param{NoOutOfOffice};
}
else {
$CacheKey = join '::', 'GetUserData', 'UserID',
$Param{UserID},
$Param{Valid},
$FirstnameLastNameOrder,
$Param{NoOutOfOffice};
}
# check cache
my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get(
Type => $Self->{CacheType},
Key => $CacheKey,
);
return %{$Cache} if $Cache;
# get initial data
my @Bind;
my $SQL = "SELECT $Self->{UserTableUserID}, $Self->{UserTableUser}, "
. " title, first_name, last_name, $Self->{UserTableUserPW}, valid_id, "
. " create_time, change_time FROM $Self->{UserTable} WHERE ";
if ( $Param{User} ) {
my $User = lc $Param{User};
$SQL .= " $Self->{Lower}($Self->{UserTableUser}) = ?";
push @Bind, \$User;
}
else {
$SQL .= " $Self->{UserTableUserID} = ?";
push @Bind, \$Param{UserID};
}
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
return if !$DBObject->Prepare(
SQL => $SQL,
Bind => \@Bind,
Limit => 1,
);
my %Data;
while ( my @Row = $DBObject->FetchrowArray() ) {
$Data{UserID} = $Row[0];
$Data{UserLogin} = $Row[1];
$Data{UserTitle} = $Row[2];
$Data{UserFirstname} = $Row[3];
$Data{UserLastname} = $Row[4];
$Data{UserPw} = $Row[5];
$Data{ValidID} = $Row[6];
$Data{CreateTime} = $Row[7];
$Data{ChangeTime} = $Row[8];
}
# check data
if ( !$Data{UserID} ) {
if ( $Param{User} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'notice',
Message => "No UserData for user: '$Param{User}'.",
);
return;
}
else {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'notice',
Message => "No UserData for user id: '$Param{UserID}'.",
);
return;
}
}
# Store CacheTTL locally so that we can reduce it for users that are out of office.
my $CacheTTL = $Self->{CacheTTL};
# check valid, return if there is locked for valid users
if ( $Param{Valid} ) {
my $Hit = 0;
for ( $Kernel::OM->Get('Kernel::System::Valid')->ValidIDsGet() ) {
if ( $_ eq $Data{ValidID} ) {
$Hit = 1;
}
}
if ( !$Hit ) {
# set cache
$Kernel::OM->Get('Kernel::System::Cache')->Set(
Type => $Self->{CacheType},
TTL => $CacheTTL,
Key => $CacheKey,
Value => {},
);
return;
}
}
# generate the full name and save it in the hash
my $UserFullname = $Self->_UserFullname(
%Data,
NameOrder => $FirstnameLastNameOrder,
);
# save the generated fullname in the hash.
$Data{UserFullname} = $UserFullname;
# get preferences
my %Preferences = $Self->GetPreferences( UserID => $Data{UserID} );
my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime');
# add last login timestamp
if ( $Preferences{UserLastLogin} ) {
my $UserLastLoginTimeObj = $Kernel::OM->Create(
'Kernel::System::DateTime',
ObjectParams => {
Epoch => $Preferences{UserLastLogin}
}
);
$Preferences{UserLastLoginTimestamp} = $UserLastLoginTimeObj->ToString();
}
# check compat stuff
if ( !$Preferences{UserEmail} ) {
$Preferences{UserEmail} = $Data{UserLogin};
}
# out of office check
if ( !$Param{NoOutOfOffice} ) {
if ( $Preferences{OutOfOffice} ) {
my $CurrentTimeObject = $Kernel::OM->Create('Kernel::System::DateTime');
my $CreateDTObject = sub {
my %Param = @_;
return $Kernel::OM->Create(
'Kernel::System::DateTime',
ObjectParams => {
String => sprintf(
'%d-%02d-%02d %s',
$Param{Year},
$Param{Month},
$Param{Day},
$Param{Time}
),
},
);
};
my $TimeStartObj = $CreateDTObject->(
Year => $Preferences{OutOfOfficeStartYear},
Month => $Preferences{OutOfOfficeStartMonth},
Day => $Preferences{OutOfOfficeStartDay},
Time => '00:00:00',
);
my $TimeEndObj = $CreateDTObject->(
Year => $Preferences{OutOfOfficeEndYear},
Month => $Preferences{OutOfOfficeEndMonth},
Day => $Preferences{OutOfOfficeEndDay},
Time => '23:59:59',
);
if ( $TimeStartObj < $CurrentTimeObject && $TimeEndObj > $CurrentTimeObject ) {
my $OutOfOfficeMessageTemplate =
$ConfigObject->Get('OutOfOfficeMessageTemplate') || '*** out of office until %s (%s d left) ***';
my $TillDate = sprintf(
'%04d-%02d-%02d',
$Preferences{OutOfOfficeEndYear},
$Preferences{OutOfOfficeEndMonth},
$Preferences{OutOfOfficeEndDay}
);
my $Till = int( ( $TimeEndObj->ToEpoch() - $CurrentTimeObject->ToEpoch() ) / 60 / 60 / 24 );
$Preferences{OutOfOfficeMessage} = sprintf( $OutOfOfficeMessageTemplate, $TillDate, $Till );
$Data{UserFullname} .= ' ' . $Preferences{OutOfOfficeMessage};
}
# Reduce CacheTTL to one hour for users that are out of office to make sure the cache expires timely
# even if there is no update action.
$CacheTTL = 60 * 60 * 1;
}
}
# merge hash
%Data = ( %Data, %Preferences );
# add preferences defaults
my $Config = $ConfigObject->Get('PreferencesGroups');
if ( $Config && ref $Config eq 'HASH' ) {
KEY:
for my $Key ( sort keys %{$Config} ) {
next KEY if !defined $Config->{$Key}->{DataSelected};
# check if data is defined
next KEY if defined $Data{ $Config->{$Key}->{PrefKey} };
# set default data
$Data{ $Config->{$Key}->{PrefKey} } = $Config->{$Key}->{DataSelected};
}
}
# set cache
$Kernel::OM->Get('Kernel::System::Cache')->Set(
Type => $Self->{CacheType},
TTL => $CacheTTL,
Key => $CacheKey,
Value => \%Data,
);
return %Data;
}
=head2 UserAdd()
to add new users
my $UserID = $UserObject->UserAdd(
UserFirstname => 'Huber',
UserLastname => 'Manfred',
UserLogin => 'mhuber',
UserPw => 'some-pass', # not required
UserEmail => 'email@example.com',
UserMobile => '1234567890', # not required
ValidID => 1,
ChangeUserID => 123,
);
=cut
sub UserAdd {
my ( $Self, %Param ) = @_;
# check needed stuff
for (qw(UserFirstname UserLastname UserLogin UserEmail ValidID ChangeUserID)) {
if ( !$Param{$_} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $_!",
);
return;
}
}
# check if a user with this login (username) already exits
if ( $Self->UserLoginExistsCheck( UserLogin => $Param{UserLogin} ) ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "A user with the username '$Param{UserLogin}' already exists.",
);
return;
}
# check email address
if (
$Param{UserEmail}
&& !$Kernel::OM->Get('Kernel::System::CheckItem')->CheckEmail( Address => $Param{UserEmail} )
&& grep { $_ eq $Param{ValidID} } $Kernel::OM->Get('Kernel::System::Valid')->ValidIDsGet()
)
{
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Email address ($Param{UserEmail}) not valid ("
. $Kernel::OM->Get('Kernel::System::CheckItem')->CheckError() . ")!",
);
return;
}
# check password
if ( !$Param{UserPw} ) {
$Param{UserPw} = $Self->GenerateRandomPassword();
}
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# Don't store the user's password in plaintext initially. It will be stored in a
# hashed version later with SetPassword().
my $RandomPassword = $Self->GenerateRandomPassword();
# sql
return if !$DBObject->Do(
SQL => "INSERT INTO $Self->{UserTable} "
. "(title, first_name, last_name, "
. " $Self->{UserTableUser}, $Self->{UserTableUserPW}, "
. " valid_id, create_time, create_by, change_time, change_by)"
. " VALUES "
. " (?, ?, ?, ?, ?, ?, current_timestamp, ?, current_timestamp, ?)",
Bind => [
\$Param{UserTitle}, \$Param{UserFirstname}, \$Param{UserLastname},
\$Param{UserLogin}, \$RandomPassword, \$Param{ValidID},
\$Param{ChangeUserID}, \$Param{ChangeUserID},
],
);
# get new user id
my $UserLogin = lc $Param{UserLogin};
return if !$DBObject->Prepare(
SQL => "SELECT $Self->{UserTableUserID} FROM $Self->{UserTable} "
. " WHERE $Self->{Lower}($Self->{UserTableUser}) = ?",
Bind => [ \$UserLogin ],
Limit => 1,
);
# fetch the result
my $UserID;
while ( my @Row = $DBObject->FetchrowArray() ) {
$UserID = $Row[0];
}
# check if user exists
if ( !$UserID ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'notice',
Message => "Unable to create User: '$Param{UserLogin}' ($Param{ChangeUserID})!",
);
return;
}
# log notice
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'notice',
Message =>
"User: '$Param{UserLogin}' ID: '$UserID' created successfully ($Param{ChangeUserID})!",
);
# set password
$Self->SetPassword(
UserLogin => $Param{UserLogin},
PW => $Param{UserPw}
);
my @UserPreferences = grep { $_ =~ m{^User}xsi } sort keys %Param;
USERPREFERENCE:
for my $UserPreference (@UserPreferences) {
# UserEmail is required.
next USERPREFERENCE if $UserPreference eq 'UserEmail' && !$Param{UserEmail};
# Set user preferences.
# Native user data will not be overwriten (handeled by SetPreferences()).
$Self->SetPreferences(
UserID => $UserID,
Key => $UserPreference,
Value => $Param{$UserPreference} || '',
);
}
# delete cache
$Kernel::OM->Get('Kernel::System::Cache')->CleanUp(
Type => $Self->{CacheType},
);
return $UserID;
}
=head2 UserUpdate()
to update users
$UserObject->UserUpdate(
UserID => 4321,
UserFirstname => 'Huber',
UserLastname => 'Manfred',
UserLogin => 'mhuber',
UserPw => 'some-pass', # not required
UserEmail => 'email@example.com',
UserMobile => '1234567890', # not required
ValidID => 1,
ChangeUserID => 123,
);
=cut
sub UserUpdate {
my ( $Self, %Param ) = @_;
# check needed stuff
for (qw(UserID UserFirstname UserLastname UserLogin ValidID ChangeUserID)) {
if ( !$Param{$_} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $_!",
);
return;
}
}
# store old user login for later use
my $OldUserLogin = $Self->UserLookup(
UserID => $Param{UserID},
);
# check if a user with this login (username) already exists
if (
$Self->UserLoginExistsCheck(
UserLogin => $Param{UserLogin},
UserID => $Param{UserID}
)
)
{
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "A user with the username '$Param{UserLogin}' already exists.",
);
return;
}
# check email address
if (
$Param{UserEmail}
&& !$Kernel::OM->Get('Kernel::System::CheckItem')->CheckEmail( Address => $Param{UserEmail} )
&& grep { $_ eq $Param{ValidID} } $Kernel::OM->Get('Kernel::System::Valid')->ValidIDsGet()
)
{
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Email address ($Param{UserEmail}) not valid ("
. $Kernel::OM->Get('Kernel::System::CheckItem')->CheckError() . ")!",
);
return;
}
# update db
return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
SQL => "UPDATE $Self->{UserTable} SET title = ?, first_name = ?, last_name = ?, "
. " $Self->{UserTableUser} = ?, valid_id = ?, "
. " change_time = current_timestamp, change_by = ? "
. " WHERE $Self->{UserTableUserID} = ?",
Bind => [
\$Param{UserTitle}, \$Param{UserFirstname}, \$Param{UserLastname},
\$Param{UserLogin}, \$Param{ValidID}, \$Param{ChangeUserID}, \$Param{UserID},
],
);
# check pw
if ( $Param{UserPw} ) {
$Self->SetPassword(
UserLogin => $Param{UserLogin},
PW => $Param{UserPw}
);
}
my @UserPreferences = grep { $_ =~ m{^User}xsi } sort keys %Param;
USERPREFERENCE:
for my $UserPreference (@UserPreferences) {
# UserEmail is required.
next USERPREFERENCE if $UserPreference eq 'UserEmail' && !$Param{UserEmail};
# Set user preferences.
# Native user data will not be overwriten (handeled by SetPreferences()).
$Self->SetPreferences(
UserID => $Param{UserID},
Key => $UserPreference,
Value => $Param{$UserPreference} || '',
);
}
# update search profiles if the UserLogin changed
if ( lc $OldUserLogin ne lc $Param{UserLogin} ) {
$Kernel::OM->Get('Kernel::System::SearchProfile')->SearchProfileUpdateUserLogin(
Base => 'TicketSearch',
UserLogin => $OldUserLogin,
NewUserLogin => $Param{UserLogin},
);
}
$Self->_UserCacheClear( UserID => $Param{UserID} );
# TODO Not needed to delete the cache if ValidID or Name was not changed
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
my $SystemPermissionConfig = $Kernel::OM->Get('Kernel::Config')->Get('System::Permission') || [];
for my $Type ( @{$SystemPermissionConfig}, 'rw' ) {
$CacheObject->Delete(
Type => 'GroupPermissionUserGet',
Key => 'PermissionUserGet::' . $Param{UserID} . '::' . $Type,
);
}
$CacheObject->CleanUp(
Type => 'GroupPermissionGroupGet',
);
return 1;
}
=head2 UserSearch()
to search users
my %List = $UserObject->UserSearch(
Search => '*some*', # also 'hans+huber' possible
Valid => 1, # not required
);
my %List = $UserObject->UserSearch(
UserLogin => '*some*',
Limit => 50,
Valid => 1, # not required
);
my %List = $UserObject->UserSearch(
PostMasterSearch => 'email@example.com',
Valid => 1, # not required
);
Returns hash of UserID, Login pairs:
my %List = (
1 => 'root@locahost',
4 => 'admin',
9 => 'joe',
);
For PostMasterSearch, it returns hash of UserID, Email pairs:
my %List = (
4 => 'john@example.com',
9 => 'joe@example.com',
);
=cut
sub UserSearch {
my ( $Self, %Param ) = @_;
my %Users;
my $Valid = $Param{Valid} // 1;
# check needed stuff
if ( !$Param{Search} && !$Param{UserLogin} && !$Param{PostMasterSearch} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need Search, UserLogin or PostMasterSearch!',
);
return;
}
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# get like escape string needed for some databases (e.g. oracle)
my $LikeEscapeString = $DBObject->GetDatabaseFunction('LikeEscapeString');
# build SQL string
my $SQL = "SELECT $Self->{UserTableUserID}, login
FROM $Self->{UserTable} WHERE ";
my @Bind;
if ( $Param{Search} ) {
my %QueryCondition = $DBObject->QueryCondition(
Key => [qw(login first_name last_name)],
Value => $Param{Search},
BindMode => 1,
);
$SQL .= $QueryCondition{SQL} . ' ';
push @Bind, @{ $QueryCondition{Values} };
}
elsif ( $Param{PostMasterSearch} ) {
my %UserID = $Self->SearchPreferences(
Key => 'UserEmail',
Value => $Param{PostMasterSearch},
);
for ( sort keys %UserID ) {
my %User = $Self->GetUserData(
UserID => $_,
Valid => $Param{Valid},
);
if (%User) {
return %UserID;
}
}
return;
}
elsif ( $Param{UserLogin} ) {
my $UserLogin = lc $Param{UserLogin};
$SQL .= " $Self->{Lower}($Self->{UserTableUser}) LIKE ? $LikeEscapeString";
$UserLogin =~ s/\*/%/g;
$UserLogin = $DBObject->Quote( $UserLogin, 'Like' );
push @Bind, \$UserLogin;
}
# add valid option
if ($Valid) {
$SQL .= "AND valid_id IN ("
. join( ', ', $Kernel::OM->Get('Kernel::System::Valid')->ValidIDsGet() ) . ")";
}
# get data
return if !$DBObject->Prepare(
SQL => $SQL,
Bind => \@Bind,
Limit => $Self->{UserSearchListLimit} || $Param{Limit},
);
# fetch the result
while ( my @Row = $DBObject->FetchrowArray() ) {
$Users{ $Row[0] } = $Row[1];
}
return %Users;
}
=head2 SetPassword()
to set users passwords
$UserObject->SetPassword(
UserLogin => 'some-login',
PW => 'some-new-password'
);
=cut
sub SetPassword {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{UserLogin} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need UserLogin!'
);
return;
}
# get old user data
my %User = $Self->GetUserData( User => $Param{UserLogin} );
if ( !$User{UserLogin} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'No such User!',
);
return;
}
my $Pw = $Param{PW} || '';
my $CryptedPw = '';
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
my $CryptType = $ConfigObject->Get('AuthModule::DB::CryptType') || 'sha2';
# crypt plain (no crypt at all)
if ( $CryptType eq 'plain' ) {
$CryptedPw = $Pw;
}
# crypt with UNIX crypt
elsif ( $CryptType eq 'crypt' ) {
# encode output, needed by crypt() only non utf8 signs
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Pw );
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Param{UserLogin} );
$CryptedPw = crypt( $Pw, $Param{UserLogin} );
}
# crypt with md5
elsif ( $CryptType eq 'md5' || !$CryptType ) {
# encode output, needed by unix_md5_crypt() only non utf8 signs
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Pw );
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Param{UserLogin} );
$CryptedPw = unix_md5_crypt( $Pw, $Param{UserLogin} );
}
# crypt with md5 (compatible with Apache's .htpasswd files)
elsif ( $CryptType eq 'apr1' ) {
# encode output, needed by unix_md5_crypt() only non utf8 signs
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Pw );
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Param{UserLogin} );
$CryptedPw = apache_md5_crypt( $Pw, $Param{UserLogin} );
}
# crypt with sha1
elsif ( $CryptType eq 'sha1' ) {
my $SHAObject = Digest::SHA->new('sha1');
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Pw );
$SHAObject->add($Pw);
$CryptedPw = $SHAObject->hexdigest();
}
# crypt with sha512
elsif ( $CryptType eq 'sha512' ) {
my $SHAObject = Digest::SHA->new('sha512');
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Pw );
$SHAObject->add($Pw);
$CryptedPw = $SHAObject->hexdigest();
}
# bcrypt
elsif ( $CryptType eq 'bcrypt' ) {
if ( !$Kernel::OM->Get('Kernel::System::Main')->Require('Crypt::Eksblowfish::Bcrypt') ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message =>
"User: '$User{UserLogin}' tried to store password with bcrypt but 'Crypt::Eksblowfish::Bcrypt' is not installed!",
);
return;
}
my $Cost = $ConfigObject->Get('AuthModule::DB::bcryptCost') // 12;
# Don't allow values smaller than 9 for security.
$Cost = 9 if $Cost < 9;
# Current Crypt::Eksblowfish::Bcrypt limit is 31.
$Cost = 31 if $Cost > 31;
my $Salt = $Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString( Length => 16 );
# remove UTF8 flag, required by Crypt::Eksblowfish::Bcrypt
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Pw );
# calculate password hash
my $Octets = Crypt::Eksblowfish::Bcrypt::bcrypt_hash(
{
key_nul => 1,
cost => $Cost,
salt => $Salt,
},
$Pw
);
# We will store cost and salt in the password string so that it can be decoded
# in future even if we use a higher cost by default.
$CryptedPw = "BCRYPT:$Cost:$Salt:" . Crypt::Eksblowfish::Bcrypt::en_base64($Octets);
}
# crypt with sha256 as fallback
else {
my $SHAObject = Digest::SHA->new('sha256');
# encode output, needed by sha256_hex() only non utf8 signs
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Pw );
$SHAObject->add($Pw);
$CryptedPw = $SHAObject->hexdigest();
}
# update db
my $UserLogin = lc $Param{UserLogin};
return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
SQL => "UPDATE $Self->{UserTable} SET $Self->{UserTableUserPW} = ? "
. " WHERE $Self->{Lower}($Self->{UserTableUser}) = ?",
Bind => [ \$CryptedPw, \$UserLogin ],
);
# log notice
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'notice',
Message => "User: '$Param{UserLogin}' changed password successfully!",
);
return 1;
}
=head2 UserLookup()
user login or id lookup
my $UserLogin = $UserObject->UserLookup(
UserID => 1,
Silent => 1, # optional, don't generate log entry if user was not found
);
my $UserID = $UserObject->UserLookup(
UserLogin => 'some_user_login',
Silent => 1, # optional, don't generate log entry if user was not found
);
=cut
sub UserLookup {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{UserLogin} && !$Param{UserID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need UserLogin or UserID!'
);
return;
}
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
if ( $Param{UserLogin} ) {
# check cache
my $CacheKey = 'UserLookup::ID::' . $Param{UserLogin};
my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get(
Type => $Self->{CacheType},
Key => $CacheKey,
);
return $Cache if $Cache;
# build sql query
my $UserLogin = lc $Param{UserLogin};
return if !$DBObject->Prepare(
SQL => "SELECT $Self->{UserTableUserID} FROM $Self->{UserTable} "
. " WHERE $Self->{Lower}($Self->{UserTableUser}) = ?",
Bind => [ \$UserLogin ],
Limit => 1,
);
# fetch the result
my $ID;
while ( my @Row = $DBObject->FetchrowArray() ) {
$ID = $Row[0];
}
if ( !$ID ) {
if ( !$Param{Silent} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "No UserID found for '$Param{UserLogin}'!",
);
}
return;
}
# set cache
$Kernel::OM->Get('Kernel::System::Cache')->Set(
Type => $Self->{CacheType},
TTL => $Self->{CacheTTL},
Key => $CacheKey,
Value => $ID,
);
return $ID;
}
else {
# check cache
my $CacheKey = 'UserLookup::Login::' . $Param{UserID};
my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get(
Type => $Self->{CacheType},
Key => $CacheKey,
);
return $Cache if $Cache;
# build sql query
return if !$DBObject->Prepare(
SQL => "SELECT $Self->{UserTableUser} FROM $Self->{UserTable} "
. " WHERE $Self->{UserTableUserID} = ?",
Bind => [ \$Param{UserID} ],
Limit => 1,
);
# fetch the result
my $Login;
while ( my @Row = $DBObject->FetchrowArray() ) {
$Login = $Row[0];
}
if ( !$Login ) {
if ( !$Param{Silent} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "No UserLogin found for '$Param{UserID}'!",
);
}
return;
}
# set cache
$Kernel::OM->Get('Kernel::System::Cache')->Set(
Type => $Self->{CacheType},
TTL => $Self->{CacheTTL},
Key => $CacheKey,
Value => $Login,
);
return $Login;
}
}
=head2 UserName()
get user name
my $Name = $UserObject->UserName(
User => 'some-login',
);
or
my $Name = $UserObject->UserName(
UserID => 123,
);
=cut
sub UserName {
my ( $Self, %Param ) = @_;
my %User = $Self->GetUserData(%Param);
return if !%User;
return $User{UserFullname};
}
=head2 UserList()
return a hash with all users
my %List = $UserObject->UserList(
Type => 'Short', # Short|Long, default Short
Valid => 1, # default 1
NoOutOfOffice => 1, # (optional) default 0
);
=cut
sub UserList {
my ( $Self, %Param ) = @_;
my $Type = $Param{Type} || 'Short';
# set valid option
my $Valid = $Param{Valid} // 1;
# get config object
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
# get configuration for the full name order
my $FirstnameLastNameOrder = $ConfigObject->Get('FirstnameLastnameOrder') || 0;
my $NoOutOfOffice = $Param{NoOutOfOffice} || 0;
# check cache
my $CacheKey = join '::', 'UserList', $Type, $Valid, $FirstnameLastNameOrder, $NoOutOfOffice;
my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get(
Type => $Self->{CacheType},
Key => $CacheKey,
);
return %{$Cache} if $Cache;
my $SelectStr;
if ( $Type eq 'Short' ) {
$SelectStr = "$ConfigObject->{DatabaseUserTableUserID}, "
. " $ConfigObject->{DatabaseUserTableUser}";
}
else {
$SelectStr = "$ConfigObject->{DatabaseUserTableUserID}, "
. " last_name, first_name, "
. " $ConfigObject->{DatabaseUserTableUser}";
}
my $SQL = "SELECT $SelectStr FROM $ConfigObject->{DatabaseUserTable}";
# sql query
if ($Valid) {
$SQL
.= " WHERE valid_id IN ( ${\(join ', ', $Kernel::OM->Get('Kernel::System::Valid')->ValidIDsGet())} )";
}
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
return if !$DBObject->Prepare( SQL => $SQL );
# fetch the result
my %UsersRaw;
my %Users;
while ( my @Row = $DBObject->FetchrowArray() ) {
$UsersRaw{ $Row[0] } = \@Row;
}
if ( $Type eq 'Short' ) {
for my $CurrentUserID ( sort keys %UsersRaw ) {
$Users{$CurrentUserID} = $UsersRaw{$CurrentUserID}->[1];
}
}
else {
for my $CurrentUserID ( sort keys %UsersRaw ) {
my @Data = @{ $UsersRaw{$CurrentUserID} };
my $UserFullname = $Self->_UserFullname(
UserFirstname => $Data[2],
UserLastname => $Data[1],
UserLogin => $Data[3],
NameOrder => $FirstnameLastNameOrder,
);
$Users{$CurrentUserID} = $UserFullname;
}
}
# check vacation option
if ( !$NoOutOfOffice ) {
USERID:
for my $UserID ( sort keys %Users ) {
next USERID if !$UserID;
my %User = $Self->GetUserData(
UserID => $UserID,
);
if ( $User{OutOfOfficeMessage} ) {
$Users{$UserID} .= ' ' . $User{OutOfOfficeMessage};
}
}
}
# set cache
$Kernel::OM->Get('Kernel::System::Cache')->Set(
Type => $Self->{CacheType},
TTL => $Self->{CacheTTL},
Key => $CacheKey,
Value => \%Users,
);
return %Users;
}
=head2 GenerateRandomPassword()
generate a random password
my $Password = $UserObject->GenerateRandomPassword();
or
my $Password = $UserObject->GenerateRandomPassword(
Size => 16,
);
=cut
sub GenerateRandomPassword {
my ( $Self, %Param ) = @_;
# generated passwords are eight characters long by default.
my $Size = $Param{Size} || 8;
my $Password = $Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString(
Length => $Size,
);
return $Password;
}
=head2 SetPreferences()
set user preferences
$UserObject->SetPreferences(
Key => 'UserComment',
Value => 'some comment',
UserID => 123,
);
=cut
sub SetPreferences {
my ( $Self, %Param ) = @_;
# check needed stuff
for (qw(Key UserID)) {
if ( !$Param{$_} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $_!"
);
return;
}
}
# Don't allow overwriting of native user data.
my %Blacklisted = (
UserID => 1,
UserLogin => 1,
UserPw => 1,
UserFirstname => 1,
UserLastname => 1,
UserFullname => 1,
UserTitle => 1,
ChangeTime => 1,
CreateTime => 1,
ValidID => 1,
);
return 0 if $Blacklisted{ $Param{Key} };
# get current setting
my %User = $Self->GetUserData(
UserID => $Param{UserID},
NoOutOfOffice => 1,
);
# no updated needed
return 1
if defined $User{ $Param{Key} }
&& defined $Param{Value}
&& $User{ $Param{Key} } eq $Param{Value};
$Self->_UserCacheClear( UserID => $Param{UserID} );
# get user preferences config
my $GeneratorModule = $Kernel::OM->Get('Kernel::Config')->Get('User::PreferencesModule')
|| 'Kernel::System::User::Preferences::DB';
# get generator preferences module
my $PreferencesObject = $Kernel::OM->Get($GeneratorModule);
# set preferences
return $PreferencesObject->SetPreferences(%Param);
}
sub _UserCacheClear {
my ( $Self, %Param ) = @_;
if ( !$Param{UserID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need UserID!"
);
return;
}
my $Login = $Self->UserLookup( UserID => $Param{UserID} );
my @CacheKeys;
# Delete cache for all possible FirstnameLastNameOrder settings as this might be overridden by users.
for my $FirstnameLastNameOrder ( 0 .. 8 ) {
for my $ActiveLevel1 ( 0 .. 1 ) {
for my $ActiveLevel2 ( 0 .. 1 ) {
push @CacheKeys, (
"GetUserData::User::${Login}::${ActiveLevel1}::${FirstnameLastNameOrder}::${ActiveLevel2}",
"GetUserData::UserID::$Param{UserID}::${ActiveLevel1}::${FirstnameLastNameOrder}::${ActiveLevel2}",
"UserList::Short::${ActiveLevel1}::${FirstnameLastNameOrder}::${ActiveLevel2}",
"UserList::Long::${ActiveLevel1}::${FirstnameLastNameOrder}::${ActiveLevel2}",
);
}
}
push @CacheKeys, (
'UserLookup::ID::' . $Login,
'UserLookup::Login::' . $Param{UserID},
);
}
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
for my $CacheKey (@CacheKeys) {
$CacheObject->Delete(
Type => $Self->{CacheType},
Key => $CacheKey,
);
}
return 1;
}
=head2 GetPreferences()
get user preferences
my %Preferences = $UserObject->GetPreferences(
UserID => 123,
);
=cut
sub GetPreferences {
my ( $Self, %Param ) = @_;
# get user preferences config
my $GeneratorModule = $Kernel::OM->Get('Kernel::Config')->Get('User::PreferencesModule')
|| 'Kernel::System::User::Preferences::DB';
# get generator preferences module
my $PreferencesObject = $Kernel::OM->Get($GeneratorModule);
return $PreferencesObject->GetPreferences(%Param);
}
=head2 SearchPreferences()
search in user preferences
my %UserList = $UserObject->SearchPreferences(
Key => 'UserEmail',
Value => 'email@example.com', # optional, limit to a certain value/pattern
);
=cut
sub SearchPreferences {
my ( $Self, %Param ) = @_;
# get user preferences config
my $GeneratorModule = $Kernel::OM->Get('Kernel::Config')->Get('User::PreferencesModule')
|| 'Kernel::System::User::Preferences::DB';
# get generator preferences module
my $PreferencesObject = $Kernel::OM->Get($GeneratorModule);
return $PreferencesObject->SearchPreferences(%Param);
}
=head2 TokenGenerate()
generate a random token
my $Token = $UserObject->TokenGenerate(
UserID => 123,
);
=cut
sub TokenGenerate {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{UserID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need UserID!"
);
return;
}
my $Token = $Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString(
Length => 15,
);
# save token in preferences
$Self->SetPreferences(
Key => 'UserToken',
Value => $Token,
UserID => $Param{UserID},
);
return $Token;
}
=head2 TokenCheck()
check password token
my $Valid = $UserObject->TokenCheck(
Token => $Token,
UserID => 123,
);
=cut
sub TokenCheck {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{Token} || !$Param{UserID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need Token and UserID!'
);
return;
}
# get preferences token
my %Preferences = $Self->GetPreferences(
UserID => $Param{UserID},
);
# check requested vs. stored token
if ( $Preferences{UserToken} && $Preferences{UserToken} eq $Param{Token} ) {
# reset password token
$Self->SetPreferences(
Key => 'UserToken',
Value => '',
UserID => $Param{UserID},
);
# return true if token is valid
return 1;
}
# return false if token is invalid
return;
}
=begin Internal:
=head2 _UserFullname()
Builds the user fullname based on firstname, lastname and login. The order
can be configured.
my $Fullname = $Object->_UserFullname(
UserFirstname => 'Test',
UserLastname => 'Person',
UserLogin => 'tp',
NameOrder => 0, # optional 0, 1, 2, 3, 4, 5
);
=cut
sub _UserFullname {
my ( $Self, %Param ) = @_;
for my $Needed (qw(UserFirstname UserLastname UserLogin)) {
if ( !$Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!",
);
return;
}
}
my $FirstnameLastNameOrder = $Param{NameOrder} || 0;
my $UserFullname;
if ( $FirstnameLastNameOrder eq '0' ) {
$UserFullname = $Param{UserFirstname} . ' '
. $Param{UserLastname};
}
elsif ( $FirstnameLastNameOrder eq '1' ) {
$UserFullname = $Param{UserLastname} . ', '
. $Param{UserFirstname};
}
elsif ( $FirstnameLastNameOrder eq '2' ) {
$UserFullname = $Param{UserFirstname} . ' '
. $Param{UserLastname} . ' ('
. $Param{UserLogin} . ')';
}
elsif ( $FirstnameLastNameOrder eq '3' ) {
$UserFullname = $Param{UserLastname} . ', '
. $Param{UserFirstname} . ' ('
. $Param{UserLogin} . ')';
}
elsif ( $FirstnameLastNameOrder eq '4' ) {
$UserFullname = '(' . $Param{UserLogin}
. ') ' . $Param{UserFirstname}
. ' ' . $Param{UserLastname};
}
elsif ( $FirstnameLastNameOrder eq '5' ) {
$UserFullname = '(' . $Param{UserLogin}
. ') ' . $Param{UserLastname}
. ', ' . $Param{UserFirstname};
}
elsif ( $FirstnameLastNameOrder eq '6' ) {
$UserFullname = $Param{UserLastname} . ' '
. $Param{UserFirstname};
}
elsif ( $FirstnameLastNameOrder eq '7' ) {
$UserFullname = $Param{UserLastname} . ' '
. $Param{UserFirstname} . ' ('
. $Param{UserLogin} . ')';
}
elsif ( $FirstnameLastNameOrder eq '8' ) {
$UserFullname = '(' . $Param{UserLogin}
. ') ' . $Param{UserLastname}
. ' ' . $Param{UserFirstname};
}
elsif ( $FirstnameLastNameOrder eq '9' ) {
$UserFullname = $Param{UserLastname} . $Param{UserFirstname};
}
return $UserFullname;
}
=end Internal:
=cut
=head2 UserLoginExistsCheck()
return 1 if another user with this login (username) already exists
$Exist = $UserObject->UserLoginExistsCheck(
UserLogin => 'Some::UserLogin',
UserID => 1, # optional
);
=cut
sub UserLoginExistsCheck {
my ( $Self, %Param ) = @_;
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
return if !$DBObject->Prepare(
SQL =>
"SELECT $Self->{UserTableUserID} FROM $Self->{UserTable} WHERE $Self->{UserTableUser} = ?",
Bind => [ \$Param{UserLogin} ],
);
# fetch the result
my $Flag;
while ( my @Row = $DBObject->FetchrowArray() ) {
if ( !$Param{UserID} || $Param{UserID} ne $Row[0] ) {
$Flag = 1;
}
}
if ($Flag) {
return 1;
}
return 0;
}
1;
=head1 TERMS AND CONDITIONS
This software is part of the OTRS project (L<https://otrs.org/>).
This software comes with ABSOLUTELY NO WARRANTY. For details, see
the enclosed file COPYING for license information (GPL). If you
did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
=cut