diff --git a/src/main/perl/Process.pm b/src/main/perl/Process.pm index e906afd..3ef7bcb 100644 --- a/src/main/perl/Process.pm +++ b/src/main/perl/Process.pm @@ -2,7 +2,7 @@ use parent qw(CAF::Object); -use LC::Exception qw (SUCCESS throw_error); +use LC::Exception qw (SUCCESS); use LC::Process; use File::Which; @@ -11,6 +11,8 @@ use File::Basename; use overload ('""' => 'stringify_command'); use Readonly; +use English; + Readonly::Hash my %LC_PROCESS_DISPATCH => { output => \&LC::Process::output, toutput => \&LC::Process::toutput, @@ -19,7 +21,6 @@ Readonly::Hash my %LC_PROCESS_DISPATCH => { execute => \&LC::Process::execute, }; - =pod =head1 NAME @@ -138,6 +139,27 @@ This does not cover command output. If the output (stdout and/or stderr) contain sensitve information, make sure to handle it yourself via C and/or C options (or by using the C method). +=item C + +Run command as effective user. The C can be an id (all digits) or a name. + +This only works when the current user is root. + +In case a non-root user uses this option, or C is not a valid user, +the initialisation will work but any actual execution will fail. + +=item C + +Run command with effective group. The C can be an id (all digits) or a name. + +If C is defined, and C is not, the users primary group will be used +(instead of the default root group). + +This only works when the current user is root. + +In case a non-root user uses this option, or C is not a valid group, +the initialisation will work but any actual execution will fail. + =back These options will only be used by the execute method. @@ -157,7 +179,9 @@ sub _initialize $self->{NoAction} = 0 }; - $self->{sensitive} = $opts{sensitive}; + foreach my $name (qw(sensitive user group)) { + $self->{$name} = $opts{$name}; + } $self->{COMMAND} = $command; @@ -225,10 +249,141 @@ C<< command: [ ] >>. =cut +# if mode is user, return uid and primary gid of +# the user in the user attribute ($self->{user}). +# if mode is group, return gid and undef of +# the group in the group attribute ($self->{group}). +sub _get_uid_gid +{ + my ($self, $mode) = @_; + + my @res; + my $target = $self->{$mode}; + if (defined($target)) { + my $is_id = $target =~ m/^\d+$/ ? 1 : 0; + my $is_user = $mode eq 'user' ? 1 : 0; + # This is ugly + # But you cannot reference the builtin functions, + # maybe by using simple wrapper like my $fn = sub { builtin(@_) } (eg sub {getpwname($_[0])}) + # But the getpw / getgr functions are safe to use (they do not die, just return empty list) + # so no _safe_eval and a funcref required + # For the is_id case, strictly not needed to check details, since setuid can change to non-known user + # But we don't allow that here. + my @info = $is_id ? + ($is_user ? getpwuid($target) : getgrgid($target)) : + ($is_user ? getpwnam($target) : getgrnam($target)); + + # What do we need from info: the IDs, and for users, also the primary groups + if (@info) { + # pwnam/pwuid: uid=2 and gid=3 + # grnam/uid: gid=2 + @res = ($info[2], $is_user ? $info[3] : undef); + $self->verbose("Got $mode id $res[0] ", $is_user ? "(gid $res[1])" : '', + " (is_id $is_id is_user $is_user)"); + } else { + $self->error("No such $mode $target (is user $is_user; is id $is_id)"); + } + } + + return @res; +} + +# minimal functions for mocking +sub _uid {return $UID;}; +sub _euid {return $EUID;}; +sub _gid {return "$GID";}; # as string +sub _egid {return "$EGID";}; # as string +sub _set_euid {$EUID = $_[0]; return $!}; +sub _set_egid {$EGID = $_[0]; return $!}; + +# set euid or egid, with verification and reporting +# return 1 or 0 +# report error on failure +sub _set_uid_gid +{ + my ($self, $target, $set, $get, $name, $suff, $action) = @_; + + return 1 if ! defined($target); + + my $value = &$get; + my $msg = "$name from '$value' to $suff"; + # stringification to handle numeric case + if ("$value" eq "$target") { + $self->verbose(ucfirst($action)." $msg: no changes required") + } else { + my $err = &$set($target); + $value = &$get; + if ("$value" eq "$target") { + $self->verbose(ucfirst($action)." $msg") + } else { + $self->error("Something went wrong $action $msg: new $name '$value', ", + ((defined($err) && $err ne '') ? "reason $err" : "no reason given")); + return 0; + } + } + return 1; +}; + + +# set euid/egid if user and/or group was set +# returns 1 on success. +# on failure, report error and return undef +sub _set_eff_user_group +{ + my ($self, $orig) = @_; + + my ($uid, $gid, $pri_gid, $gid_full, $action); + + my $restore = defined($orig) ? 1 : 0; + + if ($restore) { + $action = "restoring"; + ($uid, $gid) = @$orig; + # We assume the original gid is the original list of groups + $gid_full = "$gid"; + } else { + $action = "changing"; + + # has to be array context + ($uid, $pri_gid) = $self->_get_uid_gid('user'); + ($gid, undef) = $self->_get_uid_gid('group'); + # use user primary group when no group specified + $gid = $pri_gid if defined $uid && ! defined $gid; + # This is how you set the GID to only the GID (i.e. no other groups) + $gid_full = "$gid $gid" if defined $gid; + } + + my $set_user = sub { + return $self->_set_uid_gid($uid, \&_set_euid, \&_euid, 'EUID', + "$uid with UID "._uid(), $action); + }; + + # return 1 or 0 + my $set_group = sub { + return $self->_set_uid_gid($gid_full, \&_set_egid, \&_egid, 'EGID', + "'$gid_full' with GID '"._gid()."'", $action); + }; + + my $res = 0; + if ($restore) { + # first restore user + $res += &$set_user; + $res += &$set_group if $res; + } else { + # first set group + # new euid might not have sufficient permissions to change the gid + $res += &$set_group; + $res += &$set_user if $res; + } + + return $res == 2 ? 1 : 0; +} + sub _LC_Process { my ($self, $function, $args, $noaction_value, $msg, $postmsg) = @_; + my $res; $msg =~ s/^(\w)/Not \L$1/ if $self->noAction(); $self->verbose("$msg command: ", $self->_sensitive_commandline(), (defined($postmsg) ? " $postmsg" : '')); @@ -236,16 +391,31 @@ sub _LC_Process if ($self->noAction()) { $self->debug(1, "LC_Process in noaction mode for $function"); $? = 0; - return $noaction_value; + $res = $noaction_value; } else { - my $funcref = $LC_PROCESS_DISPATCH{$function}; - if (defined($funcref)) { - return $funcref->(@$args); - } else { - $self->error("Unsupported LC::Process function $function"); - return; + + my $current_user_group = !(defined $self->{user} or defined $self->{group}); + + # The original GID (as list of groups) + # Only relevant if curent_user_group is false + my $orig_user_group; + $orig_user_group = [_uid, _gid] if !$current_user_group; + + if ($current_user_group or $self->_set_eff_user_group()) { + my $funcref = $LC_PROCESS_DISPATCH{$function}; + if (defined($funcref)) { + $res = $funcref->(@$args); + } else { + $self->error("Unsupported LC::Process function $function"); + $res = undef; + } } + + # always try to restore + $self->_set_eff_user_group($orig_user_group) if !$current_user_group; } + + return $res; } =back diff --git a/src/test/perl/process.t b/src/test/perl/process.t index afb1c0e..82ce0f1 100644 --- a/src/test/perl/process.t +++ b/src/test/perl/process.t @@ -1,6 +1,18 @@ use strict; use warnings; +# hello mocking +my $iddata; +my $idcalled; +BEGIN { + # Before CAF::Process + *CORE::GLOBAL::getpwuid = sub {$idcalled->{getpwuid} += 1;return @{$iddata->{getpwuid}};}; + *CORE::GLOBAL::getpwnam = sub {$idcalled->{getpwnam} += 1;return @{$iddata->{getpwnam}};}; + *CORE::GLOBAL::getgrnam = sub {$idcalled->{getgrnam} += 1;return @{$iddata->{getgrnam}};}; + *CORE::GLOBAL::getgrgid = sub {$idcalled->{getgrgid} += 1;return @{$iddata->{getgrgid}};}; +} + + use FindBin qw($Bin); use lib "$Bin/modules"; use testapp; @@ -11,6 +23,29 @@ use Test::Quattor::Object; use Test::MockModule; my $mock = Test::MockModule->new ("CAF::Process"); +# After CAF::Process +no warnings 'redefine'; +*CAF::Process::_uid = sub {$idcalled->{uid} += 1;return $iddata->{uid};}; +*CAF::Process::_euid = sub {$idcalled->{euid} += 1;return $iddata->{euid};}; +*CAF::Process::_gid = sub {$idcalled->{gid} += 1;return $iddata->{gid};}; +*CAF::Process::_egid = sub {$idcalled->{egid} += 1;return $iddata->{egid};}; +*CAF::Process::_set_euid = sub {$idcalled->{seuid} += 1;$iddata->{euid} = $_[0];}; +*CAF::Process::_set_egid = sub {$idcalled->{segid} += 1;$iddata->{egid} = $_[0];}; +use warnings 'redefine'; + +$iddata = { + uid => 122, + euid => 122, + gid => "123 123 10 20 30", + egid => "123 123 10 20 30", + getpwuid => [qw(myself x 122 123)], + getpwnam => [qw(myself x 122 123)], + getgrgid => [qw(mygroup alias 123 myself)], + getgrnam => [qw(mygroup alias 123 myself)], +}; + +my $obj = Test::Quattor::Object->new(); + my ($p, $this_app, $str, $fh, $out, $out2); our ($run, $trun, $execute, $output, $toutput) = (0, 0, 0, 0, 0); @@ -38,6 +73,10 @@ open ($fh, ">", \$str); $this_app = testapp->new ($0, qw (--verbose)); $this_app->config_reporter(logfile => $fh); +=head2 Test no logging + +=cut + $p = CAF::Process->new ($command); $p->execute (); is ($execute, 1, "execute called with no logging"); @@ -64,6 +103,11 @@ ok (@$cmd == @$command, "Correct command called by toutput"); ok (!defined ($str), "Nothing logged by toutput"); init_test(); + +=head2 Test with logging + +=cut + # Let's test this with a few options, especially logging. $p = CAF::Process->new ($command, log => $this_app, stdin => "Something"); @@ -158,6 +202,10 @@ is($ps->_sensitive_commandline(), 'ls (sensitive function failed, contact developers)', "expected commandline w sensitive=funcref and failure"); +=head2 pushargs / setopts + +=cut + init_test(); # Let's test the rest of the commands $p->pushargs (qw (this does not matter at all)); @@ -170,7 +218,11 @@ ok (@$cmd == @$command, "The command got options appended"); $str = undef; $p->setopts (stdout => \$str); is($str, "", "Stdout is initialized"); -# Test the NoAction flag + +=head2 Test the NoAction flag + +=cut + $CAF::Object::NoAction = 1; init_test(); $p = CAF::Process->new($command); @@ -192,11 +244,19 @@ ok(!$p->{NoAction}, $p = CAF::Process->new($command, keeps_state => 0); ok($p->{NoAction}, "Respect NoAction if the command changes the state"); +=head2 stringification + +=cut + $p = CAF::Process->new($command); my $command_str = join(" ", @$command); is($p->stringify_command, $command_str, "stringify_command returns joined command"); is("$p", $command_str, "overloaded stringification"); +=head2 is_executable / get_executable + +=cut + is(join(" ", @{$p->get_command}), $command_str, "get_command returns ref to command list"); is($p->get_executable, "ls", "get_executable returns executable"); @@ -217,5 +277,213 @@ $p = CAF::Process->new([]); is("$p", "", "Empty command process is empty string"); ok(! $p, "Empty process is logical false (autogeneration of overloaded bool via new stringify)"); +=head2 _set_eff_user_group / _set_uid_gid / _get_uid_gid + +=cut + +$p = CAF::Process->new ($command, log => $obj); +$p->{user} = undef; +$p->{group} = undef; +is_deeply([$p->_get_uid_gid('user')], [], "_get_uid_gid user returns empty array on missing user attr"); +is_deeply([$p->_get_uid_gid('group')], [], "_get_uid_gid group returns empty array on missing group attr"); + +$idcalled = {}; +$p->{user} = 'x'; +is_deeply([$p->_get_uid_gid('user')], [122, 123], "get_uid_gid user returns uid and prim gid"); +is_deeply($idcalled, {getpwnam => 1}, 'get_uid_gid user used pwnam'); + +$idcalled = {}; +$p->{user} = 122; +is_deeply([$p->_get_uid_gid('user')], [122, 123], "get_uid_gid user id returns uid and prim gid"); +is_deeply($idcalled, {getpwuid => 1}, 'get_uid_gid user id used pwuid'); + +$idcalled = {}; +$p->{group} = 'x'; +is_deeply([$p->_get_uid_gid('group')], [123, undef], "get_uid_gid group returns gid and undef"); +is_deeply($idcalled, {getgrnam => 1}, 'get_uid_gid group used grnam'); + +$idcalled = {}; +$p->{group} = 123; +is_deeply([$p->_get_uid_gid('group')], [123, undef], "get_uid_gid group id returns gid and undef"); +is_deeply($idcalled, {getgrgid => 1}, 'get_uid_gid group id used grgid'); + +# unknown userid +$idcalled = {}; +$iddata->{getpwuid} = []; # empty list means unknown user +$p->{user} = 122; +is_deeply([$p->_get_uid_gid('user')], [], "get_uid_gid user id returns empty array with unknown userid"); +is_deeply($idcalled, {getpwuid => 1}, 'get_uid_gid user id used pwuid with unknown userid'); +is($obj->{LOGLATEST}->{ERROR}, 'No such user 122 (is user 1; is id 1)', + 'error reported with with unknown userid'); + + +$idcalled = {}; +$p->{user} = 'x'; +$iddata->{getpwuid} = [qw(myself x 122 123)]; +$p->{group} = undef; + +my $value = []; +my $valueidx = 0; +my $setargs = []; + +my $get = sub { + $valueidx += 1; + # If there's an error, the message will be "No such file or directory" + $! = 2; + return $value->[$valueidx-1]; +}; +my $set = sub {push(@$setargs, \@_); return $!}; + +$valueidx = 0; +$setargs = []; +$value = [123]; +ok($p->_set_uid_gid(123, $set, $get, "something", "suffix", "update"), + "set_uid_gid returns ok if target is current value"); +is_deeply($setargs, [], "set not called, target is current value"); + +$valueidx = 0; +$setargs = []; +$value = [122, 123]; +ok($p->_set_uid_gid(123, $set, $get, "something", "suffix", "update"), + "set_uid_gid returns ok when target set"); +is_deeply($setargs, [[123]], "set called with target success"); + +$valueidx = 0; +$setargs = []; +$value = [122, 122]; +ok(!$p->_set_uid_gid(123, $set, $get, "something", "suffix", "update"), + "set_uid_gid fails when set failed"); +is_deeply($setargs, [[123]], "set called with target failure"); +is($obj->{LOGLATEST}->{ERROR}, + "Something went wrong update something from '122' to suffix: new something '122', reason No such file or directory", + "error reported when failed to set target"); + +$valueidx = 0; +$setargs = []; +$value = [1, 1]; # success +$mock->mock('_set_uid_gid', sub { + # can't compare the methods for some reason + push(@$setargs, [$_[1], $_[4], $_[5], $_[6]]); + $valueidx += 1; + return $value->[$valueidx-1]; +}); + +$valueidx = 0; +$setargs = []; +$value = [1, 1]; # success +ok($p->_set_eff_user_group(), "_set_eff_user_group ok"); +is_deeply($setargs, [ + ['123 123', 'EGID', "'123 123' with GID '123 123 10 20 30'", 'changing'], + ['122', 'EUID', '122 with UID 122', 'changing'], +], "_set_uid_gid called as expected (EGID before EUID)"); + +$valueidx = 0; +$setargs = []; +$value = [0, 1]; # one failure +ok(! $p->_set_eff_user_group(), "_set_eff_user_group failed 1st"); + +is_deeply($setargs, [ + ['123 123', 'EGID', "'123 123' with GID '123 123 10 20 30'", 'changing'], +], "_set_uid_gid failed, only first called"); + +$valueidx = 0; +$setargs = []; +$value = [1, 0]; # one failure +ok(! $p->_set_eff_user_group(), "_set_eff_user_group failed 2nd"); +is_deeply($setargs, [ + ['123 123', 'EGID', "'123 123' with GID '123 123 10 20 30'", 'changing'], + ['122', 'EUID', '122 with UID 122', 'changing'], +], "_set_uid_gid failed, first and second called"); + +# restore original +$valueidx = 0; +$setargs = []; +$value = [1, 1]; # success +my $orig = [124, "124 124 80 90"]; +ok($p->_set_eff_user_group($orig), "_set_eff_user_group ok restore"); +diag explain $setargs; +is_deeply($setargs, [ + ['124', 'EUID', '124 with UID 122', 'restoring'], + ['124 124 80 90', 'EGID', "'124 124 80 90' with GID '123 123 10 20 30'", 'restoring'], +], "_set_uid_gid called as expected (EUID before EGID) restore"); + +$valueidx = 0; +$setargs = []; +$value = [0, 1]; # one failure +ok(! $p->_set_eff_user_group($orig), "_set_eff_user_group failed 1st restor"); + +is_deeply($setargs, [ + ['124', 'EUID', '124 with UID 122', 'restoring'], +], "_set_uid_gid failed, only first called restore"); + +$valueidx = 0; +$setargs = []; +$value = [1, 0]; # one failure +ok(! $p->_set_eff_user_group($orig), "_set_eff_user_group failed 2nd restore"); +is_deeply($setargs, [ + ['124', 'EUID', '124 with UID 122', 'restoring'], + ['124 124 80 90', 'EGID', "'124 124 80 90' with GID '123 123 10 20 30'", 'restoring'], +], "_set_uid_gid failed, first and second called restore"); + +=head2 run as user + +=cut + +# insert the mocking here +my $args; +$mock->mock('_set_eff_user_group', sub {shift; push(@$args, \@_); return 1}); +$CAF::Object::NoAction = 1; +$execute = 0; +$idcalled = {}; +$p = CAF::Process->new ($command, log => $obj); +$p->execute (); +is ($execute, 0, "execute called with for user test w NoAction=1"); +is_deeply($idcalled, {}, "none of the user methods called w NoAction=1"); +ok(!defined $args, "_set_eff_user_group not called w NoAction=1"); + +$CAF::Object::NoAction = 0; +$execute = 0; +$idcalled = {}; +$p = CAF::Process->new ($command, log => $obj); +$p->execute (); +is ($execute, 1, "execute called with for user test w/o user"); +is_deeply($idcalled, {}, "none of the user methods called w/o user"); +ok(!defined $args, "_set_eff_user_group not called w/o user"); + +$CAF::Object::NoAction = 1; +$execute = 0; +$idcalled = {}; +$p = CAF::Process->new ($command, log => $obj, user => 'x'); +$p->execute (); +is ($execute, 0, "execute called with for user test w NoAction=1 with user"); +is_deeply($idcalled, {}, "none of the user methods called w NoAction=1 with user"); +ok(!defined $args, "_set_eff_user_group not called wNoAction=1 with user"); +$p = CAF::Process->new ($command, log => $obj, group => 'x'); +$p->execute (); +is ($execute, 0, "execute called with for user test w NoAction=1 with group"); +is_deeply($idcalled, {}, "none of the user methods called w NoAction=1 with group"); +ok(!defined $args, "_set_eff_user_group not called wNoAction=1 with group"); + +$CAF::Object::NoAction = 0; +$execute = 0; +$idcalled = {}; +$p = CAF::Process->new ($command, log => $obj, user => 'x'); +$p->execute (); +is ($execute, 1, "execute called with for user test w user"); +is_deeply($idcalled, {uid => 1, gid => 1}, + "uid/gid called (to determine orig user/group) w user"); +is_deeply($args, [[], [[122, "123 123 10 20 30"]]], + "_set_eff_user_group called w/o args first and then with original uid/gid args w user"); + +$args = undef; +$execute = 0; +$idcalled = {}; +$p = CAF::Process->new ($command, log => $obj, group => 'x'); +$p->execute (); +is ($execute, 1, "execute called with for user test w group"); +is_deeply($idcalled, {uid => 1, gid => 1}, + "uid/gid called (to determine orig user/group) w group"); +is_deeply($args, [[], [[122, "123 123 10 20 30"]]], + "_set_eff_user_group called w/o args first and then with original uid/gid args w group"); done_testing();