Skip to content

Commit

Permalink
Process: run process as another user
Browse files Browse the repository at this point in the history
  • Loading branch information
stdweird authored and jrha committed Sep 5, 2024
1 parent f50b380 commit d9ce1f4
Show file tree
Hide file tree
Showing 2 changed files with 449 additions and 11 deletions.
190 changes: 180 additions & 10 deletions src/main/perl/Process.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -19,7 +21,6 @@ Readonly::Hash my %LC_PROCESS_DISPATCH => {
execute => \&LC::Process::execute,
};


=pod
=head1 NAME
Expand Down Expand Up @@ -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<stdout> and/or C<stderr>
options (or by using the C<output> method).
=item C<user>
Run command as effective user. The C<user> 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<user> is not a valid user,
the initialisation will work but any actual execution will fail.
=item C<group>
Run command with effective group. The C<group> can be an id (all digits) or a name.
If C<user> is defined, and C<group> 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<group> 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.
Expand All @@ -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;

Expand Down Expand Up @@ -225,27 +249,173 @@ C<< <msg> command: <COMMAND>[ <postmsg>] >>.
=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" : ''));

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
Expand Down
Loading

0 comments on commit d9ce1f4

Please sign in to comment.