# # (c) Jan Gehring <jan.gehring@gmail.com> # # vim: set ts=2 sw=2 tw=0: # vim: set expandtab: package Rex::User::Linux; use strict; use warnings; our $VERSION = '1.7.0'; # 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;