#
# (c) Jan Gehring <jan.gehring@gmail.com>
#
# vim: set ts=2 sw=2 tw=0:
# vim: set expandtab:
package Rex::User::Linux;
use 5.010001;
use strict;
use warnings;
our $VERSION = '1.13.4'; # VERSION
use Rex::Logger;
require Rex::Commands;
use Rex::Commands::MD5;
use Rex::Helper::Run;
use Rex::Helper::Encode;
use Rex::Commands::Fs;
use Rex::Interface::File;
use Rex::Interface::Fs;
use Rex::Interface::Exec;
use Rex::Helper::Path;
use JSON::MaybeXS;
use Rex::User::Base;
use base qw(Rex::User::Base);
sub new {
my $that = shift;
my $proto = ref($that) || $that;
my $self = $proto->SUPER::new(@_);
bless( $self, $proto );
return $self;
}
sub create_user {
my ( $self, $user, $data ) = @_;
my $cmd;
my $uid = $self->get_uid($user);
my $run_cmd = 0;
my $should_create_home;
# if any home creation intent has been defined,
# don't follow the default home creation policy
my $use_default_home_policy =
( defined $data->{'create_home'}
|| defined $data->{'create-home'}
|| defined $data->{'no_create_home'}
|| defined $data->{'no-create-home'} ) ? 0 : 1;
if ( !$use_default_home_policy ) {
if ( $data->{'create_home'} || $data->{'create-home'} ) {
$should_create_home = 1;
}
elsif ( $data->{'no_create_home'} || $data->{'no-create-home'} ) {
$should_create_home = 0;
}
elsif (
( exists $data->{'no_create_home'} && $data->{'no_create_home'} == 0 )
|| ( exists $data->{'no-create-home'} && $data->{'no-create-home'} == 0 )
)
{
$should_create_home = 1;
}
}
if ( !defined $uid ) {
Rex::Logger::debug("User $user does not exists. Creating it now.");
$cmd = "/usr/sbin/useradd ";
if ( exists $data->{system} ) {
$cmd .= " -r";
}
$run_cmd = 1;
}
else {
# only the user should be there, no modifications.
# so just return
if ( !defined $data ) {
return {
changed => 0,
uid => $uid,
};
}
Rex::Logger::debug("User $user already exists. Updating...");
if ( exists $data->{uid} && $data->{uid} == $uid ) {
delete $data->{uid};
}
$cmd = "/usr/sbin/usermod ";
}
if ( exists $data->{non_uniq} ) {
$cmd .= " -o ";
$run_cmd = 1;
}
if ( exists $data->{uid} ) {
$cmd .= " --uid " . $data->{uid};
$run_cmd = 1;
}
if ( exists $data->{home} ) {
$run_cmd = 1;
$cmd .= " -d " . $data->{home};
# don't create home directory in useradd mode if it already exists
$should_create_home = 0 if ( !defined $uid && is_dir( $data->{home} ) );
}
if ( !$use_default_home_policy ) {
if ( !defined $uid ) { #useradd mode
if ($should_create_home) {
$cmd .= " -m ";
}
else {
$cmd .= " -M ";
}
}
else { #usermod mode
$cmd .= " -m " if ( exists $data->{home} );
}
}
if ( exists $data->{shell} ) {
$run_cmd = 1;
$cmd .= " --shell " . $data->{shell};
}
if ( exists $data->{comment} ) {
$run_cmd = 1;
$cmd .= " --comment '" . $data->{comment} . "'";
}
if ( exists $data->{expire} ) {
$run_cmd = 1;
$cmd .= " --expiredate '" . $data->{expire} . "'";
}
if ( exists $data->{groups} ) {
$run_cmd = 1;
my @groups = @{ $data->{groups} };
my $pri_group = shift @groups;
$cmd .= " --gid $pri_group";
if (@groups) {
$cmd .= " --groups " . join( ",", @groups );
}
}
my $old_pw_md5 = md5("/etc/passwd");
my $old_sh_md5 = "";
eval { $old_sh_md5 = md5("/etc/shadow"); };
# only run the cmd if needed
if ($run_cmd) {
my $rnd_file = get_tmp_file;
my $fh = Rex::Interface::File->create;
$fh->open( ">", $rnd_file );
$fh->write("rm \$0\n$cmd $user\nexit \$?\n");
$fh->close;
i_run "/bin/sh $rnd_file", fail_ok => 1;
if ( $? == 0 ) {
Rex::Logger::debug("User $user created/updated.");
}
else {
Rex::Logger::info( "Error creating/updating user $user", "warn" );
die("Error creating/updating user $user");
}
}
if ( exists $data->{password} ) {
my $rnd_file = get_tmp_file;
my $fh = Rex::Interface::File->create;
$fh->open( ">", $rnd_file );
$fh->write( "rm \$0\n/bin/echo -e '"
. $data->{password} . "\\n"
. $data->{password}
. "' | /usr/bin/passwd $user\nexit \$?\n" );
$fh->close;
Rex::Logger::debug("Changing password of $user.");
i_run "/bin/sh $rnd_file", fail_ok => 1;
if ( $? != 0 ) {
die("Error setting password for $user");
}
}
if ( exists $data->{crypt_password} && $data->{crypt_password} ) {
my $rnd_file = get_tmp_file;
my $fh = Rex::Interface::File->create;
$fh->open( ">", $rnd_file );
$fh->write( "rm \$0\nusermod -p '"
. $data->{crypt_password}
. "' $user\nexit \$?\n" );
$fh->close;
Rex::Logger::debug("Setting encrypted password of $user");
i_run "/bin/sh $rnd_file", fail_ok => 1;
if ( $? != 0 ) {
die("Error setting password for $user");
}
}
my $new_pw_md5 = md5("/etc/passwd");
my $new_sh_md5 = "";
eval { $new_sh_md5 = md5("/etc/shadow"); };
if ( $new_pw_md5 eq $old_pw_md5 && $new_sh_md5 eq $old_sh_md5 ) {
return {
changed => 0,
ret => $self->get_uid($user),
};
}
else {
return {
changed => 1,
ret => $self->get_uid($user),
},
;
}
}
sub rm_user {
my ( $self, $user, $data ) = @_;
Rex::Logger::debug("Removing user $user");
my $cmd = "/usr/sbin/userdel";
if ( exists $data->{delete_home} ) {
$cmd .= " --remove";
}
if ( exists $data->{force} ) {
$cmd .= " --force";
}
my $output = i_run $cmd . " " . $user, fail_ok => 1;
if ( $? == 6 ) {
Rex::Logger::info( "Cannot delete user $user (no such user)", "warn" );
}
elsif ( $? != 0 ) {
die("Error deleting user $user ($output)");
}
}
sub get_uid {
my ( $self, $user ) = @_;
my %data = $self->get_user($user);
return $data{uid};
}
sub user_groups {
my ( $self, $user ) = @_;
Rex::Logger::debug("Getting group membership of $user");
my $rnd_file = get_tmp_file;
my $fh = Rex::Interface::File->create;
my $script = q|
unlink $0;
$exe = "/usr/bin/groups";
if(! -x $exe) {
$exe = "/bin/groups";
} print to_json([ map {chomp; $_ =~ s/^[^:]*:\s*(.*)\s*$/$1/; split / /, $_} qx{$exe $ARGV[0]} ]);
|;
$fh->open( ">", $rnd_file );
$fh->write($script);
$fh->write( func_to_json() );
$fh->close;
my $data_str = i_run "perl $rnd_file $user", fail_ok => 1;
if ( $? != 0 ) {
die("Error getting group list");
}
my $data = decode_json($data_str);
my $wantarray = wantarray();
if ( defined $wantarray && !$wantarray ) {
# arrayref
return $data;
}
return @{$data};
}
sub user_list {
my $self = shift;
Rex::Logger::debug("Getting user list");
my $rnd_file = get_tmp_file;
my $script = q|
unlink $0;
print to_json([ map {chomp; $_ =~ s/^([^:]*):.*$/$1/; $_} qx{/usr/bin/getent passwd} ]);
|;
my $fh = Rex::Interface::File->create;
$fh->open( ">", $rnd_file );
$fh->write($script);
$fh->write( func_to_json() );
$fh->close;
my $data_str = i_run "perl $rnd_file", fail_ok => 1;
if ( $? != 0 ) {
die("Error getting user list");
}
my $data = decode_json($data_str);
return @$data;
}
sub get_user {
my ( $self, $user ) = @_;
Rex::Logger::debug("Getting information for $user");
my $rnd_file = get_tmp_file;
my $fh = Rex::Interface::File->create;
my $script = q|
unlink $0;
print to_json([ getpwnam($ARGV[0]) ]);
|;
$fh->open( ">", $rnd_file );
$fh->write($script);
$fh->write( func_to_json() );
$fh->close;
my $data_str = i_run "perl $rnd_file $user", fail_ok => 1;
if ( $? != 0 ) {
die("Error getting user information for $user");
}
my $data = decode_json($data_str);
return (
name => $data->[0],
password => $data->[1],
uid => $data->[2],
gid => $data->[3],
comment => $data->[5],
home => $data->[7],
shell => $data->[8],
expire => exists $data->[9] ? $data->[9] : 0,
);
}
sub lock_password {
my ( $self, $user ) = @_;
# Is the password already locked?
my $result = i_run "passwd --status $user", fail_ok => 1;
die "Unexpected result from passwd: $result"
unless $result =~ /^$user\s+(L|NP|P)\s+/;
if ( $1 eq 'L' ) {
# Already locked
return { changed => 0 };
}
else {
my $ret = i_run "passwd --lock $user", fail_ok => 1;
if ( $? != 0 ) {
die("Error locking account $user: $ret");
}
return {
changed => 1,
ret => $ret,
};
}
}
sub unlock_password {
my ( $self, $user ) = @_;
# Is the password already unlocked?
my $result = i_run "passwd --status $user", fail_ok => 1;
die "Unexpected result from passwd: $result"
unless $result =~ /^$user\s+(L|NP|P)\s+/;
if ( $1 eq 'P' ) {
# Already unlocked
return { changed => 0 };
}
else {
# Capture error string on failure (eg. account has no password)
my ( $ret, $err ) = i_run "passwd --unlock $user", sub { @_ }, fail_ok => 1;
if ( $? != 0 ) {
die("Error unlocking account $user: $err");
}
return {
changed => 1,
ret => $ret,
};
}
}
sub create_group {
my ( $self, $group, $data ) = @_;
my $cmd;
my $gid = $self->get_gid($group);
if ( !defined $gid ) {
Rex::Logger::debug("Creating new group $group");
$cmd = "/usr/sbin/groupadd ";
}
elsif ( exists $data->{gid} && $data->{gid} == $gid ) {
if ( Rex::Config->get_do_reporting ) {
return {
changed => 0,
ret => $gid,
};
}
return $gid;
}
else {
if ( !defined $data ) {
if ( Rex::Config->get_do_reporting ) {
return {
changed => 0,
ret => $gid,
};
}
return $gid;
}
Rex::Logger::debug("Group $group already exists. Updating...");
$cmd = "/usr/sbin/groupmod ";
}
if ( exists $data->{gid} ) {
$cmd .= " -g " . $data->{gid};
$gid = undef;
}
i_run $cmd . " " . $group, fail_ok => 1;
if ( $? != 0 ) {
die("Error creating/modifying group $group");
}
if ( defined $gid ) {
return $gid;
}
return $self->get_gid($group);
}
sub get_gid {
my ( $self, $group ) = @_;
my %data = $self->get_group($group);
return $data{gid};
}
sub get_group {
my ( $self, $group ) = @_;
Rex::Logger::debug("Getting information for $group");
my @data =
split(
" ",
""
. i_run(
"perl -le 'print join(\" \", getgrnam(\$ARGV[0]));' '$group'",
fail_ok => 1
),
4
);
if ( $? != 0 ) {
die("Error getting group information");
}
return (
name => $data[0],
password => $data[1],
gid => $data[2],
members => $data[3],
);
}
sub rm_group {
my ( $self, $group ) = @_;
i_run "/usr/sbin/groupdel $group", fail_ok => 1;
if ( $? != 0 ) {
die("Error deleting group $group");
}
}
1;