Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Process: run process as another user #257

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
jrha marked this conversation as resolved.
Show resolved Hide resolved
{
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