From a7a9b96afd50575d7431da8b34ee6efa2bc1f8be Mon Sep 17 00:00:00 2001 From: Rafael Medina Date: Sat, 26 Sep 2026 21:16:19 -0600 Subject: [PATCH 01/10] Adding DOAS support --- bin/rex | 1 + lib/Rex.pm | 50 ++++- lib/Rex/CLI.pm | 10 + lib/Rex/Commands.pm | 3 + lib/Rex/Commands/Run.pm | 79 ++++++- lib/Rex/Group/Entry/Server.pm | 16 +- lib/Rex/Interface/Connection/Base.pm | 47 +++++ lib/Rex/Interface/Connection/Local.pm | 4 + lib/Rex/Interface/Exec/Doas.pm | 137 ++++++++++++ lib/Rex/Interface/File/Doas.pm | 19 ++ lib/Rex/Interface/Fs/Doas.pm | 293 ++++++++++++++++++++++++++ lib/Rex/Interface/Shell/Base.pm | 5 + share/rex-tab-completion.zsh | 1 + t/0.31.t | 48 ++++- 14 files changed, 708 insertions(+), 5 deletions(-) create mode 100644 lib/Rex/Interface/Exec/Doas.pm create mode 100644 lib/Rex/Interface/File/Doas.pm create mode 100644 lib/Rex/Interface/Fs/Doas.pm diff --git a/bin/rex b/bin/rex index 84da16fa6..04ea97e21 100755 --- a/bin/rex +++ b/bin/rex @@ -82,6 +82,7 @@ The C script can be used to execute tasks defined in a Rexfile from the com -O Pass additional options, like CMDB path -s Use sudo for every command -S Password for sudo + -D Use doas for every command -t Number of threads to use (aka 'parallelism' param) -v Display (R)?ex version diff --git a/lib/Rex.pm b/lib/Rex.pm index 9e37e6725..a20484dc0 100644 --- a/lib/Rex.pm +++ b/lib/Rex.pm @@ -67,8 +67,10 @@ BEGIN { eval { Net::SSH2->require; }; } -our ( @EXPORT, @CONNECTION_STACK, $GLOBAL_SUDO, $MODULE_PATHS, - $WITH_EXIT_STATUS, @FEATURE_FLAGS ); +our ( + @EXPORT, @CONNECTION_STACK, $GLOBAL_SUDO, $GLOBAL_DOAS, + $MODULE_PATHS, $WITH_EXIT_STATUS, @FEATURE_FLAGS +); $WITH_EXIT_STATUS = 1; # since 0.50 activated by default @FEATURE_FLAGS = (); @@ -385,6 +387,50 @@ sub global_sudo { Rex::Config->set_use_cache(1); } +=head2 is_doas + +Returns 1 if the current operation is executed within doas. + +=cut + +sub is_doas { + + if ( $CONNECTION_STACK[-1] ) { + if ( exists $CONNECTION_STACK[-1]->{server}->{auth}->{doas} + && $CONNECTION_STACK[-1]->{server}->{auth}->{doas} == 1 ) + { + return 1; + } + elsif ( exists $CONNECTION_STACK[-1]->{server}->{auth}->{doas} + && $CONNECTION_STACK[-1]->{server}->{auth}->{doas} == 0 ) + { + return 0; + } + } + + if ($GLOBAL_DOAS) { return 1; } + + if ( $CONNECTION_STACK[-1] ) { + return $CONNECTION_STACK[-1]->{conn}->get_current_use_doas; + } + + return 0; +} + +=head2 global_doas + +Enable or disable doas globally. + +=cut + +sub global_doas { + my ($on) = @_; + $GLOBAL_DOAS = $on; + + # turn cache on + Rex::Config->set_use_cache(1); +} + =head2 get_sftp Returns the sftp object for the current ssh connection. diff --git a/lib/Rex/CLI.pm b/lib/Rex/CLI.pm index 253b73d4e..52362d841 100644 --- a/lib/Rex/CLI.pm +++ b/lib/Rex/CLI.pm @@ -280,6 +280,7 @@ CHECK_OVERWRITE: { _handle_T(%opts); Rex::global_sudo(0); + Rex::global_doas(0); Rex::Logger::debug("Removing lockfile") if ( !exists $opts{'F'} ); CORE::unlink("$::rexfile.lock") if ( !exists $opts{'F'} ); CORE::exit 0; @@ -289,10 +290,16 @@ CHECK_OVERWRITE: { if ( exists $opts{'s'} ) { sudo("on"); } + if ( exists $opts{'S'} ) { sudo_password( $opts{'S'} ); } + # turn doas on with cli option D is used + if ( exists $opts{'D'} ) { + doas("on"); + } + if ( exists $opts{'t'} ) { parallelism( $opts{'t'} ); } @@ -439,6 +446,7 @@ sub __help__ { printf $fmt, "-O", "Pass additional options, like CMDB path"; printf $fmt, "-s", "Use sudo for every command"; printf $fmt, "-S", "Password for sudo"; + printf $fmt, "-D", "Use doas for every command"; printf $fmt, "-t", "Number of threads to use (aka 'parallelism' param)"; printf $fmt, "-v", "Display (R)?ex version"; print "\n"; @@ -649,6 +657,7 @@ sub handle_lock_file { else { Rex::Logger::debug("Found stale lock file. Removing it."); Rex::global_sudo(0); + Rex::global_doas(0); CORE::unlink("$rexfile.lock"); } } @@ -813,6 +822,7 @@ sub exit_rex { summarize($signal) if !$signal; Rex::global_sudo(0); + Rex::global_doas(0); Rex::Logger::debug("Removing lockfile") if !exists $opts{'F'}; unlink("$::rexfile.lock") if !exists $opts{'F'}; diff --git a/lib/Rex/Commands.pm b/lib/Rex/Commands.pm index 7b64c7fbc..ecb7124cb 100644 --- a/lib/Rex/Commands.pm +++ b/lib/Rex/Commands.pm @@ -1211,7 +1211,9 @@ sub LOCAL (&) { my $local_connect = Rex::Interface::Connection->create("Local"); my $old_global_sudo = $Rex::GLOBAL_SUDO; + my $old_global_doas = $Rex::GLOBAL_DOAS; $Rex::GLOBAL_SUDO = 0; + $Rex::GLOBAL_DOAS = 0; Rex::push_connection( { @@ -1230,6 +1232,7 @@ sub LOCAL (&) { Rex::pop_connection(); $Rex::GLOBAL_SUDO = $old_global_sudo; + $Rex::GLOBAL_DOAS = $old_global_doas; return $ret; } diff --git a/lib/Rex/Commands/Run.pm b/lib/Rex/Commands/Run.pm index 3f5ed885d..72edc1307 100644 --- a/lib/Rex/Commands/Run.pm +++ b/lib/Rex/Commands/Run.pm @@ -14,6 +14,7 @@ With this module you can run a command. my $output = run 'ls -l'; sudo 'id'; + doas 'id'; =head1 CONFIGURATION AND ENVIRONMENT @@ -71,7 +72,7 @@ BEGIN { use vars qw(@EXPORT); use base qw(Rex::Exporter); -@EXPORT = qw(run can_run sudo); +@EXPORT = qw(run can_run sudo doas); =head2 run($command [, $callback], %options) @@ -493,4 +494,80 @@ sub sudo { return $ret; } +=head2 doas($command) + +This function will execute the given command with doas. + +With this function you can run a command as another user via doas. + +B doas on OpenBSD does not support password input via stdin like sudo does. +You must configure /etc/doas.conf appropriately for unattended execution. +A typical configuration for a user to run commands as root without a password would be: + + permit nopass myuser as root + +However, administrators should restrict rules appropriately for their security requirements. + +You can also pass a hash reference as first argument to specify options: + + doas { user => 'root', command => 'id' }; + +doas supports the following options: + +=over 4 + +=item user + +The user to execute the command as. + +=item command + +The command to execute. + +=back + +To use doas without a password prompt (non-interactively), Rex uses the C<-n> option. +Missing authorization will cause an immediate command failure. + +=cut + +sub doas { + my ($cmd) = @_; + + my $options; + if ( ref $cmd eq "HASH" ) { + $options = $cmd; + $cmd = $options->{command}; + } + + if ( $cmd eq "on" || $cmd eq "-on" || $cmd eq "1" ) { + Rex::Logger::debug("Turning doas globally on"); + Rex::global_doas(1); + return; + } + elsif ( $cmd eq "0" ) { + Rex::Logger::debug("Turning doas globally off"); + Rex::global_doas(0); + return; + } + + Rex::get_current_connection_object()->push_use_doas(1); + Rex::get_current_connection_object()->push_doas_options( %{$options} ); + + my $ret; + + # if doas is used with a code block + if ( ref($cmd) eq "CODE" ) { + $ret = &$cmd(); + } + else { + $ret = i_run( $cmd, fail_ok => 1 ); + } + + Rex::get_current_connection_object()->pop_use_doas(); + Rex::get_current_connection_object()->pop_doas_options(); + + return $ret; +} + 1; diff --git a/lib/Rex/Group/Entry/Server.pm b/lib/Rex/Group/Entry/Server.pm index a5a4ba534..2664eab4e 100644 --- a/lib/Rex/Group/Entry/Server.pm +++ b/lib/Rex/Group/Entry/Server.pm @@ -82,6 +82,11 @@ sub new { delete $self->{sudo_password}; } + if ( $self->{doas} ) { + $self->{auth}->{doas} = $self->{doas}; + delete $self->{doas}; + } + if ( $self->{auth_type} ) { $self->{auth}->{auth_type} = $self->{auth_type}; delete $self->{auth_type}; @@ -284,12 +289,21 @@ sub get_sudo_password { Rex::Config->get_sudo_password; } +sub get_doas { + my ($self) = @_; + if ( exists $self->{auth}->{doas} ) { + return $self->{auth}->{doas}; + } + + return 0; +} + sub merge_auth { my ( $self, $other_auth ) = @_; my %new_auth; my @keys = - qw/user password port private_key public_key auth_type sudo sudo_password/; + qw/user password port private_key public_key auth_type sudo sudo_password doas/; for my $key (@keys) { my $call = "get_$key"; diff --git a/lib/Rex/Interface/Connection/Base.pm b/lib/Rex/Interface/Connection/Base.pm index d57f00016..c713affc8 100644 --- a/lib/Rex/Interface/Connection/Base.pm +++ b/lib/Rex/Interface/Connection/Base.pm @@ -20,6 +20,7 @@ sub new { bless( $self, $proto ); $self->{__sudo_options__} = []; + $self->{__doas_options__} = []; return $self; } @@ -117,4 +118,50 @@ sub run_sudo_unmodified { $self->pop_sudo_options(); } +sub push_doas_options { + my ( $self, @option ) = @_; + if ( ref $option[0] eq "HASH" ) { + push @{ $self->{__doas_options__} }, $option[0]; + } + else { + push @{ $self->{__doas_options__} }, {@option}; + } +} + +sub get_current_doas_options { + my ($self) = @_; + return $self->{__doas_options__}->[-1]; +} + +sub push_use_doas { + my ( $self, $use ) = @_; + push @{ $self->{__use_doas__} }, $use; +} + +sub get_current_use_doas { + my ($self) = @_; + + if ( $self->{is_doas} ) { + return 1; + } + return $self->{__use_doas__}->[-1]; +} + +sub pop_doas_options { + my ($self) = @_; + pop @{ $self->{__doas_options__} }; +} + +sub pop_use_doas { + my ($self) = @_; + pop @{ $self->{__use_doas__} }; +} + +sub run_doas_unmodified { + my ( $self, $code ) = @_; + $self->push_doas_options( {} ); + $code->(); + $self->pop_doas_options(); +} + 1; diff --git a/lib/Rex/Interface/Connection/Local.pm b/lib/Rex/Interface/Connection/Local.pm index eda45c2d6..5ead8f6b3 100644 --- a/lib/Rex/Interface/Connection/Local.pm +++ b/lib/Rex/Interface/Connection/Local.pm @@ -45,6 +45,10 @@ sub get_connection_type { return "Sudo"; } + if ( ( $self->{is_doas} && $self->{is_doas} == 1 ) || Rex::is_doas() ) { + return "Doas"; + } + return "Local"; } diff --git a/lib/Rex/Interface/Exec/Doas.pm b/lib/Rex/Interface/Exec/Doas.pm new file mode 100644 index 000000000..40b9b5788 --- /dev/null +++ b/lib/Rex/Interface/Exec/Doas.pm @@ -0,0 +1,137 @@ +# +# (c) Jan Gehring +# Adapted for doas support by Rafael Medina +# + +package Rex::Interface::Exec::Doas; + +use v5.14.4; +use warnings; + +our $VERSION = '9999.99.99_99'; # VERSION + +use Rex::Config; +use Rex::Interface::Exec::Local; +use Rex::Interface::Exec::SSH; +use Rex::Interface::File::Local; +use Rex::Interface::File::SSH; + +use Rex::Commands; +use Rex::Helper::Path; + +use base 'Rex::Interface::Exec::Base'; + +sub new { + my $that = shift; + my $proto = ref($that) || $that; + my $self = {@_}; + + bless( $self, $proto ); + + return $self; +} + +sub exec { + my ( $self, $cmd, $path, $option ) = @_; + + if ( exists $option->{cwd} ) { + $cmd = "cd " . $option->{cwd} . " && $cmd"; + } + + if ( exists $option->{path} ) { + $path = $option->{path}; + } + + my ( $exec, $file, $shell ); + if ( my $ssh = Rex::is_ssh() ) { + if ( ref $ssh eq "Net::OpenSSH" ) { + $exec = Rex::Interface::Exec->create("OpenSSH"); + $file = Rex::Interface::File->create("OpenSSH"); + } + else { + $exec = Rex::Interface::Exec->create("SSH"); + $file = Rex::Interface::File->create("SSH"); + } + } + else { + $exec = Rex::Interface::Exec->create("Local"); + $file = Rex::Interface::File->create("Local"); + } + $shell = Rex::Interface::Shell->create("Sh"); + + my $doas_options = + Rex::get_current_connection_object()->get_current_doas_options; + my $doas_options_str = ""; + if ( exists $doas_options->{user} ) { + $doas_options_str .= " -u " . $doas_options->{user}; + } + + if ( Rex::Config->get_sudo_without_locales() ) { + Rex::Logger::debug( + "Using doas without locales. If the locale is NOT C or en_US it will break many things!" + ); + $option->{no_locales} = 1; + } + + # doas uses -n for non-interactive (no password prompt) + my $doas_command = "doas -n $doas_options_str"; + + if ( Rex::Config->get_sudo_without_sh() ) { + Rex::Logger::debug( + "Using doas without sh will break things like file editing."); + + $shell->set_inner_shell(0); + $shell->set_doas_env(1); + + if ( exists $option->{env} ) { + $shell->set_environment( $option->{env} ); + } + } + else { + + $shell->set_locale("C"); + $shell->path($path); + + if ( Rex::Config->get_source_global_profile ) { + $shell->source_global_profile(1); + } + + if ( Rex::Config->get_source_profile ) { + $shell->source_profile(1); + } + + if ( exists $option->{env} ) { + $shell->set_environment( $option->{env} ); + } + + $shell->set_inner_shell(1); + } + + $option->{prepend_command} = $doas_command; + + my $real_exec = $shell->exec( $cmd, $option ); + Rex::Logger::debug("doas: exec: $real_exec"); + + return $exec->direct_exec( $real_exec, $option ); +} + +sub _exec { + my ( $self, $cmd, $path, $option ) = @_; + + my ( $exec, $file, $shell ); + if ( my $ssh = Rex::is_ssh() ) { + if ( ref $ssh eq "Net::OpenSSH" ) { + $exec = Rex::Interface::Exec->create("OpenSSH"); + } + else { + $exec = Rex::Interface::Exec->create("SSH"); + } + } + else { + $exec = Rex::Interface::Exec->create("Local"); + } + + return $exec->_exec( $cmd, $option ); +} + +1; diff --git a/lib/Rex/Interface/File/Doas.pm b/lib/Rex/Interface/File/Doas.pm new file mode 100644 index 000000000..c58fd7f63 --- /dev/null +++ b/lib/Rex/Interface/File/Doas.pm @@ -0,0 +1,19 @@ +# +# (c) Jan Gehring +# Adapted for doas support by Rafael Medina +# + +package Rex::Interface::File::Doas; + +use v5.14.4; +use warnings; + +our $VERSION = '9999.99.99_99'; # VERSION + +use base qw(Rex::Interface::File::Sudo); + +sub _fs { + return Rex::Interface::Fs->create("Doas"); +} + +1; diff --git a/lib/Rex/Interface/Fs/Doas.pm b/lib/Rex/Interface/Fs/Doas.pm new file mode 100644 index 000000000..4541747b2 --- /dev/null +++ b/lib/Rex/Interface/Fs/Doas.pm @@ -0,0 +1,293 @@ +# +# (c) Jan Gehring +# Adapted for doas support by Rafael Medina +# + +package Rex::Interface::Fs::Doas; + +use v5.14.4; +use warnings; + +our $VERSION = '9999.99.99_99'; # VERSION + +require Rex::Commands; +use Rex::Interface::Fs::Base; +use Rex::Helper::Path; +use Rex::Helper::Encode; +use JSON::MaybeXS; +use base qw(Rex::Interface::Fs::Base); +use Data::Dumper; + +sub new { + my $that = shift; + my $proto = ref($that) || $that; + my $self = $proto->SUPER::new(@_); + + bless( $self, $proto ); + + return $self; +} + +sub _exec { + my ( $self, $cmd, $path, $option ) = @_; + my $exec = Rex::Interface::Exec->create("Doas"); + return $exec->exec( $cmd, $path, $option ); +} + +sub ls { + my ( $self, $path ) = @_; + + my @ret; + + my @out = split( + /\n/, + $self->_exec( + "ls -a1 $path", undef, { env => { QUOTING_STYLE => "literal" } } + ) + ); + + # failed open directory, return undef + if ( $? != 0 ) { return; } + + @ret = grep { !m/^\.\.?$/ } @out; + + # return directory content + return @ret; +} + +sub upload { + my ( $self, $source, $target ) = @_; + + my $rnd_file = get_tmp_file; + + if ( my $ssh = Rex::is_ssh() ) { + if ( ref $ssh eq "Net::OpenSSH" ) { + $ssh->sftp->put( $source, $rnd_file ); + } + else { + $ssh->scp_put( $source, $rnd_file ); + } + $self->_exec("mv $rnd_file '$target'"); + } + else { + $self->cp( $source, $target ); + } + +} + +sub download { + my ( $self, $source, $target ) = @_; + + my $rnd_file = get_tmp_file; + + if ( my $ssh = Rex::is_ssh() ) { + $self->_exec("cp '$source' $rnd_file"); + $self->chmod( 444, $rnd_file ); + if ( ref $ssh eq "Net::OpenSSH" ) { + $ssh->sftp->get( $rnd_file, $target ); + } + else { + $ssh->scp_get( $rnd_file, $target ); + } + Rex::get_current_connection_object()->run_doas_unmodified( + sub { + $self->unlink($rnd_file) if -e $rnd_file; + } + ); + } + else { + $self->cp( $source, $target ); + } + +} + +sub is_dir { + my ( $self, $path ) = @_; + + ($path) = $self->_normalize_path($path); + + $self->_exec("test -d $path"); + my $ret = $?; + + $ret == 0 ? return 1 : return undef; +} + +sub is_file { + my ( $self, $file ) = @_; + + ($file) = $self->_normalize_path($file); + + $self->_exec("test -e $file"); + my $is_file = $?; + + $self->_exec("test -d $file"); + my $is_dir = $?; + + ( $is_file == 0 && $is_dir != 0 ) ? return 1 : return undef; +} + +sub unlink { + my ( $self, @files ) = @_; + (@files) = $self->_normalize_path(@files); + + $self->_exec( "rm " . join( " ", @files ) ); + if ( $? == 0 ) { return 1; } +} + +sub mkdir { + my ( $self, $dir ) = @_; + ($dir) = $self->_normalize_path($dir); + $self->_exec("mkdir $dir >/dev/null 2>&1"); + if ( $? == 0 ) { return 1; } +} + +sub stat { + my ( $self, $file ) = @_; + + my $script = q| +unlink $0; + +if(my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, + $atime, $mtime, $ctime, $blksize, $blocks) = stat($ARGV[0])) { + + my %ret; + + $ret{'mode'} = sprintf("%04o", $mode & 07777); + $ret{'size'} = $size; + $ret{'uid'} = $uid; + $ret{'gid'} = $gid; + $ret{'atime'} = $atime; + $ret{'mtime'} = $mtime; + + print to_json(%ret); +} + +|; + + $script .= func_to_json(); + + my $rnd_file = $self->_write_to_rnd_file($script); + ($file) = $self->_normalize_path($file); + my $out = $self->_exec("perl $rnd_file $file"); + + Rex::get_current_connection_object()->run_doas_unmodified( + sub { + $self->unlink($rnd_file) if -e $rnd_file; + } + ); + + if ( !$out ) { + return undef; + } + + my $tmp = decode_json($out); + + return %{$tmp}; +} + +sub is_readable { + my ( $self, $file ) = @_; + + ($file) = $self->_normalize_path($file); + $self->_exec("test -r $file"); + + if ( $? == 0 ) { return 1; } +} + +sub is_writable { + my ( $self, $file ) = @_; + + ($file) = $self->_normalize_path($file); + $self->_exec("test -w $file"); + + if ( $? == 0 ) { return 1; } +} + +sub readlink { + my ( $self, $file ) = @_; + my $script = q|unlink $0; print readlink($ARGV[0]) . "\n"; |; + ($file) = $self->_normalize_path($file); + + my $rnd_file = $self->_write_to_rnd_file($script); + my $out = $self->_exec("perl $rnd_file $file"); + my $ret = $?; + chomp $out; + Rex::get_current_connection_object()->run_doas_unmodified( + sub { + $self->unlink($rnd_file) if -e $rnd_file; + } + ); + $? = $ret; + + return $out; +} + +sub rename { + my ( $self, $old, $new ) = @_; + ($old) = $self->_normalize_path($old); + ($new) = $self->_normalize_path($new); + + $self->_exec("mv $old $new"); + + if ( $? == 0 ) { return 1; } +} + +sub glob { + my ( $self, $glob ) = @_; + + my $script = q| +unlink $0; +print to_json([ glob("| + . $glob . q|") ]); + + |; + + $script .= func_to_json(); + + my $rnd_file = $self->_write_to_rnd_file($script); + my $content = $self->_exec("perl $rnd_file"); + my $ret = $?; + Rex::get_current_connection_object()->run_doas_unmodified( + sub { + $self->unlink($rnd_file) if -e $rnd_file; + } + ); + $? = $ret; + + my $tmp = decode_json($content); + + return @{$tmp}; +} + +sub _get_file_writer { + my ($self) = @_; + + my $fh; + if ( my $o = Rex::is_ssh() ) { + if ( ref $o eq "Net::OpenSSH" ) { + $fh = Rex::Interface::File->create("OpenSSH"); + } + else { + $fh = Rex::Interface::File->create("SSH"); + } + } + else { + $fh = Rex::Interface::File->create("Local"); + } + + return $fh; +} + +sub _write_to_rnd_file { + my ( $self, $content ) = @_; + my $fh = $self->_get_file_writer(); + my $rnd_file = get_tmp_file; + + $fh->open( ">", $rnd_file ); + $fh->write($content); + $fh->close; + + return $rnd_file; +} + +1; diff --git a/lib/Rex/Interface/Shell/Base.pm b/lib/Rex/Interface/Shell/Base.pm index fe11c7176..55404210a 100644 --- a/lib/Rex/Interface/Shell/Base.pm +++ b/lib/Rex/Interface/Shell/Base.pm @@ -47,6 +47,11 @@ sub set_sudo_env { $self->{__sudo_env__} = $sudo_env; } +sub set_doas_env { + my ( $self, $doas_env ) = @_; + $self->{__doas_env__} = $doas_env; +} + sub detect { my ( $self, $con ) = @_; diff --git a/share/rex-tab-completion.zsh b/share/rex-tab-completion.zsh index a61cd7303..c8179bda8 100644 --- a/share/rex-tab-completion.zsh +++ b/share/rex-tab-completion.zsh @@ -55,6 +55,7 @@ arguments=( '-O[pass additional options, like CMDB path]' '-s[use sudo for every command]' '-S[password for sudo]' + '-D[use doas for every command]' '-t[number of threads to use (aka parallelism param)]' '-v[display (R)?ex version]' '*:options:->vary' diff --git a/t/0.31.t b/t/0.31.t index 0b55174cf..1f7b36f4c 100755 --- a/t/0.31.t +++ b/t/0.31.t @@ -5,7 +5,7 @@ use warnings; our $VERSION = '9999.99.99_99'; # VERSION -use Test::More tests => 140; +use Test::More tests => 154; use Test::Warnings; use Rex -feature => '0.31'; @@ -153,3 +153,49 @@ is( get("key3")->{name}, "foo", "got value of name parameter in key3" ); is( get("key3")->{surname}, "bar", "got value of surname parameter in key3" ); is( get("key3")->{x1}, "x", "got value of NEW name parameter x1 in key3" ); is( get("key3")->{x2}, "xx", "got value of NEW name parameter x2 in key3" ); + +# doas tests +group( "doasgroup", "doas01", "doas02" ); +task( "doastest1", group => "doasgroup", sub { } ); + +auth( + for => "doasgroup", + user => "doasuser", + password => "doaspass", + doas => TRUE() +); + +$task = Rex::TaskList->create()->get_task("doastest1"); +@all_server = @{ $task->server }; + +for my $server (@all_server) { + my $auth = $task->merge_auth($server); + is( $auth->{user}, "doasuser", "merge_auth - doas user" ); + is( $auth->{password}, "doaspass", "merge_auth - doas password" ); + is( $auth->{doas}, TRUE(), "merge_auth - doas enabled" ); + ok( !$auth->{sudo}, "merge_auth - sudo not set for doas" ); +} + +# Test global doas state +Rex::global_doas(1); +ok( Rex::is_doas(), "global doas is enabled" ); +Rex::global_doas(0); +ok( !Rex::is_doas(), "global doas is disabled" ); + +# Test doas command function +use Rex::Commands; + +# Test global doas toggle via doas command +doas "on"; +ok( Rex::is_doas(), "doas 'on' enables global doas" ); +doas "0"; +ok( !Rex::is_doas(), "doas '0' disables global doas" ); + +# Test doas with code block +my $doas_called = 0; +doas sub { $doas_called = 1; Rex::is_doas(); }; +ok( $doas_called, "doas code block executed" ); + +# Test doas with hashref options +my $ret = doas { user => 'testuser', command => 'echo test' }; +ok( defined $ret, "doas with hashref returns result" ); From 66d996f5616204658c43c186063bb56b96642f3c Mon Sep 17 00:00:00 2001 From: Rafael Medina Date: Sun, 27 Sep 2026 10:58:25 -0600 Subject: [PATCH 02/10] Reusing SUDO inside DOAS --- lib/Rex/Interface/Fs/Doas.pm | 275 +---------------------------------- 1 file changed, 3 insertions(+), 272 deletions(-) diff --git a/lib/Rex/Interface/Fs/Doas.pm b/lib/Rex/Interface/Fs/Doas.pm index 4541747b2..7bd815f47 100644 --- a/lib/Rex/Interface/Fs/Doas.pm +++ b/lib/Rex/Interface/Fs/Doas.pm @@ -10,23 +10,10 @@ use warnings; our $VERSION = '9999.99.99_99'; # VERSION -require Rex::Commands; -use Rex::Interface::Fs::Base; -use Rex::Helper::Path; -use Rex::Helper::Encode; -use JSON::MaybeXS; -use base qw(Rex::Interface::Fs::Base); -use Data::Dumper; +use Rex::Interface::Exec; +use Rex::Interface::Fs::Sudo; -sub new { - my $that = shift; - my $proto = ref($that) || $that; - my $self = $proto->SUPER::new(@_); - - bless( $self, $proto ); - - return $self; -} +use base qw(Rex::Interface::Fs::Sudo); sub _exec { my ( $self, $cmd, $path, $option ) = @_; @@ -34,260 +21,4 @@ sub _exec { return $exec->exec( $cmd, $path, $option ); } -sub ls { - my ( $self, $path ) = @_; - - my @ret; - - my @out = split( - /\n/, - $self->_exec( - "ls -a1 $path", undef, { env => { QUOTING_STYLE => "literal" } } - ) - ); - - # failed open directory, return undef - if ( $? != 0 ) { return; } - - @ret = grep { !m/^\.\.?$/ } @out; - - # return directory content - return @ret; -} - -sub upload { - my ( $self, $source, $target ) = @_; - - my $rnd_file = get_tmp_file; - - if ( my $ssh = Rex::is_ssh() ) { - if ( ref $ssh eq "Net::OpenSSH" ) { - $ssh->sftp->put( $source, $rnd_file ); - } - else { - $ssh->scp_put( $source, $rnd_file ); - } - $self->_exec("mv $rnd_file '$target'"); - } - else { - $self->cp( $source, $target ); - } - -} - -sub download { - my ( $self, $source, $target ) = @_; - - my $rnd_file = get_tmp_file; - - if ( my $ssh = Rex::is_ssh() ) { - $self->_exec("cp '$source' $rnd_file"); - $self->chmod( 444, $rnd_file ); - if ( ref $ssh eq "Net::OpenSSH" ) { - $ssh->sftp->get( $rnd_file, $target ); - } - else { - $ssh->scp_get( $rnd_file, $target ); - } - Rex::get_current_connection_object()->run_doas_unmodified( - sub { - $self->unlink($rnd_file) if -e $rnd_file; - } - ); - } - else { - $self->cp( $source, $target ); - } - -} - -sub is_dir { - my ( $self, $path ) = @_; - - ($path) = $self->_normalize_path($path); - - $self->_exec("test -d $path"); - my $ret = $?; - - $ret == 0 ? return 1 : return undef; -} - -sub is_file { - my ( $self, $file ) = @_; - - ($file) = $self->_normalize_path($file); - - $self->_exec("test -e $file"); - my $is_file = $?; - - $self->_exec("test -d $file"); - my $is_dir = $?; - - ( $is_file == 0 && $is_dir != 0 ) ? return 1 : return undef; -} - -sub unlink { - my ( $self, @files ) = @_; - (@files) = $self->_normalize_path(@files); - - $self->_exec( "rm " . join( " ", @files ) ); - if ( $? == 0 ) { return 1; } -} - -sub mkdir { - my ( $self, $dir ) = @_; - ($dir) = $self->_normalize_path($dir); - $self->_exec("mkdir $dir >/dev/null 2>&1"); - if ( $? == 0 ) { return 1; } -} - -sub stat { - my ( $self, $file ) = @_; - - my $script = q| -unlink $0; - -if(my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, - $atime, $mtime, $ctime, $blksize, $blocks) = stat($ARGV[0])) { - - my %ret; - - $ret{'mode'} = sprintf("%04o", $mode & 07777); - $ret{'size'} = $size; - $ret{'uid'} = $uid; - $ret{'gid'} = $gid; - $ret{'atime'} = $atime; - $ret{'mtime'} = $mtime; - - print to_json(%ret); -} - -|; - - $script .= func_to_json(); - - my $rnd_file = $self->_write_to_rnd_file($script); - ($file) = $self->_normalize_path($file); - my $out = $self->_exec("perl $rnd_file $file"); - - Rex::get_current_connection_object()->run_doas_unmodified( - sub { - $self->unlink($rnd_file) if -e $rnd_file; - } - ); - - if ( !$out ) { - return undef; - } - - my $tmp = decode_json($out); - - return %{$tmp}; -} - -sub is_readable { - my ( $self, $file ) = @_; - - ($file) = $self->_normalize_path($file); - $self->_exec("test -r $file"); - - if ( $? == 0 ) { return 1; } -} - -sub is_writable { - my ( $self, $file ) = @_; - - ($file) = $self->_normalize_path($file); - $self->_exec("test -w $file"); - - if ( $? == 0 ) { return 1; } -} - -sub readlink { - my ( $self, $file ) = @_; - my $script = q|unlink $0; print readlink($ARGV[0]) . "\n"; |; - ($file) = $self->_normalize_path($file); - - my $rnd_file = $self->_write_to_rnd_file($script); - my $out = $self->_exec("perl $rnd_file $file"); - my $ret = $?; - chomp $out; - Rex::get_current_connection_object()->run_doas_unmodified( - sub { - $self->unlink($rnd_file) if -e $rnd_file; - } - ); - $? = $ret; - - return $out; -} - -sub rename { - my ( $self, $old, $new ) = @_; - ($old) = $self->_normalize_path($old); - ($new) = $self->_normalize_path($new); - - $self->_exec("mv $old $new"); - - if ( $? == 0 ) { return 1; } -} - -sub glob { - my ( $self, $glob ) = @_; - - my $script = q| -unlink $0; -print to_json([ glob("| - . $glob . q|") ]); - - |; - - $script .= func_to_json(); - - my $rnd_file = $self->_write_to_rnd_file($script); - my $content = $self->_exec("perl $rnd_file"); - my $ret = $?; - Rex::get_current_connection_object()->run_doas_unmodified( - sub { - $self->unlink($rnd_file) if -e $rnd_file; - } - ); - $? = $ret; - - my $tmp = decode_json($content); - - return @{$tmp}; -} - -sub _get_file_writer { - my ($self) = @_; - - my $fh; - if ( my $o = Rex::is_ssh() ) { - if ( ref $o eq "Net::OpenSSH" ) { - $fh = Rex::Interface::File->create("OpenSSH"); - } - else { - $fh = Rex::Interface::File->create("SSH"); - } - } - else { - $fh = Rex::Interface::File->create("Local"); - } - - return $fh; -} - -sub _write_to_rnd_file { - my ( $self, $content ) = @_; - my $fh = $self->_get_file_writer(); - my $rnd_file = get_tmp_file; - - $fh->open( ">", $rnd_file ); - $fh->write($content); - $fh->close; - - return $rnd_file; -} - 1; From 87de0ee1b71abb59a6071198f6b5bfe560d1da7b Mon Sep 17 00:00:00 2001 From: Rafael Medina Date: Sun, 27 Sep 2026 15:56:00 -0600 Subject: [PATCH 03/10] Replacing literal for static string --- t/0.31.t | 105 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/t/0.31.t b/t/0.31.t index 1f7b36f4c..ac191d66b 100755 --- a/t/0.31.t +++ b/t/0.31.t @@ -23,20 +23,20 @@ no warnings; is( Rex::TaskList->create()->is_default_auth(), 0, "default auth off" ); use warnings; -group( "foo", "server1", "server2", "server3" ); -group( "bar", "serv[01..10]" ); +group( 'foo', "server1", "server2", "server3" ); +group( 'bar', "serv[01..10]" ); -my @servers = Rex::Group->get_group("foo"); +my @servers = Rex::Group->get_group('foo'); is( $servers[0], "server1", "get_group" ); is( $servers[2], "server3", "get_group" ); -@servers = Rex::Group->get_group("bar"); +@servers = Rex::Group->get_group('bar'); @servers = $servers[0]->get_servers; is( $servers[0], "serv01", "get_group with evaluation" ); is( $servers[5], "serv06", "get_group with evaluation" ); -task( "authtest1", group => "foo", sub { } ); -task( "authtest2", group => "bar", sub { } ); +task( "authtest1", group => 'foo', sub { } ); +task( "authtest2", group => 'bar', sub { } ); task( "authtest3", "srv001", sub { } ); task( "authtest4", group => "latebar", sub { } ); group( "latebar", "server[01..03]" ); @@ -64,8 +64,8 @@ for my $server (@all_server) { is( $auth->{auth_type}, "pass", "merge_auth - auth" ); } -auth( for => "bar", user => "jan", password => "foo" ); -auth( for => "latebar", user => "jan", password => "foo" ); +auth( for => 'bar', user => "jan", password => 'foo' ); +auth( for => "latebar", user => "jan", password => 'foo' ); $task = Rex::TaskList->create()->get_task("authtest1"); @all_server = @{ $task->server }; @@ -84,7 +84,7 @@ $task = Rex::TaskList->create()->get_task("authtest2"); for my $server (@all_server) { my $auth = $task->merge_auth($server); is( $auth->{user}, "jan", "merge_auth - user" ); - is( $auth->{password}, "foo", "merge_auth - pass" ); + is( $auth->{password}, 'foo', "merge_auth - pass" ); is( $auth->{public_key}, "pub.key3", "merge_auth - pub" ); is( $auth->{private_key}, "priv.key3", "merge_auth - priv" ); is( $auth->{auth_type}, "try", "merge_auth - auth_type" ); @@ -97,7 +97,7 @@ $task = Rex::TaskList->create()->get_task("authtest4"); for my $server (@all_server) { my $auth = $task->merge_auth($server); is( $auth->{user}, "jan", "merge_auth - user - lategroup" ); - is( $auth->{password}, "foo", "merge_auth - pass - lategroup" ); + is( $auth->{password}, 'foo', "merge_auth - pass - lategroup" ); is( $auth->{public_key}, "pub.key3", "merge_auth - pub - lategroup" ); is( $auth->{private_key}, "priv.key3", "merge_auth - priv - lategroup" ); is( $auth->{auth_type}, "try", "merge_auth - auth_type - lategroup" ); @@ -126,76 +126,81 @@ for my $server (@all_server) { is( $auth->{sudo}, TRUE(), "merge_auth - sudo" ); } -set( "key1", "val1" ); -is( get("key1"), "val1", "got value of key1" ); +set( 'key1', "val1" ); +is( get('key1'), "val1", "got value of key1" ); -set( "key1", "val2" ); -is( get("key1"), "val2", "got new value of key1" ); +set( 'key1', "val2" ); +is( get('key1'), "val2", "got new value of key1" ); -set( "key2", [qw/one two three/] ); -is( get("key2")->[0], "one", "got value of first item in key2" ); -is( get("key2")->[1], "two", "got value of 2nd item in key2" ); -is( get("key2")->[2], "three", "got value of 3rd item in key2" ); +set( 'key2', [qw/one two three/] ); +is( get('key2')->[0], "one", "got value of first item in key2" ); +is( get('key2')->[1], "two", "got value of 2nd item in key2" ); +is( get('key2')->[2], "three", "got value of 3rd item in key2" ); -set( "key2", [qw/four five/] ); -is( get("key2")->[0], "one", "got value of first item in key2" ); -is( get("key2")->[1], "two", "got value of 2nd item in key2" ); -is( get("key2")->[2], "three", "got value of 3rd item in key2" ); -is( get("key2")->[3], "four", "got value of NEW first item in key2" ); -is( get("key2")->[4], "five", "got value of NEW 2nd item in key2" ); +set( 'key2', [qw/four five/] ); +is( get('key2')->[0], "one", "got value of first item in key2" ); +is( get('key2')->[1], "two", "got value of 2nd item in key2" ); +is( get('key2')->[2], "three", "got value of 3rd item in key2" ); +is( get('key2')->[3], "four", "got value of NEW first item in key2" ); +is( get('key2')->[4], "five", "got value of NEW 2nd item in key2" ); -set( "key3", { name => 'foo', surname => 'bar' } ); -is( get("key3")->{name}, "foo", "got value of name parameter in key3" ); -is( get("key3")->{surname}, "bar", "got value of surname parameter in key3" ); +set( 'key3', { name => 'foo', surname => 'bar' } ); +is( get('key3')->{name}, 'foo', "got value of name parameter in key3" ); +is( get('key3')->{surname}, 'bar', "got value of surname parameter in key3" ); -set( "key3", { x1 => 'x', x2 => 'xx' } ); -is( get("key3")->{name}, "foo", "got value of name parameter in key3" ); -is( get("key3")->{surname}, "bar", "got value of surname parameter in key3" ); -is( get("key3")->{x1}, "x", "got value of NEW name parameter x1 in key3" ); -is( get("key3")->{x2}, "xx", "got value of NEW name parameter x2 in key3" ); +set( 'key3', { x1 => 'x', x2 => 'xx' } ); +is( get('key3')->{name}, 'foo', 'got value of name parameter in key3' ); +is( get('key3')->{surname}, 'bar', 'got value of surname parameter in key3' ); +is( get('key3')->{x1}, 'x', 'got value of NEW name parameter x1 in key3' ); +is( get('key3')->{x2}, 'xx', 'got value of NEW name parameter x2 in key3' ); # doas tests -group( "doasgroup", "doas01", "doas02" ); -task( "doastest1", group => "doasgroup", sub { } ); +my $doas_group = 'doasgroup'; +my $doas_task = 'doastest1'; +my $doas_user = 'doasuser'; +my $doas_pass = 'doaspass'; + +group( $doas_group, 'doas01', 'doas02' ); +task( $doas_task, group => $doas_group, sub { } ); auth( - for => "doasgroup", - user => "doasuser", - password => "doaspass", + for => $doas_group, + user => $doas_user, + password => $doas_pass, doas => TRUE() ); -$task = Rex::TaskList->create()->get_task("doastest1"); +$task = Rex::TaskList->create()->get_task($doas_task); @all_server = @{ $task->server }; for my $server (@all_server) { my $auth = $task->merge_auth($server); - is( $auth->{user}, "doasuser", "merge_auth - doas user" ); - is( $auth->{password}, "doaspass", "merge_auth - doas password" ); - is( $auth->{doas}, TRUE(), "merge_auth - doas enabled" ); - ok( !$auth->{sudo}, "merge_auth - sudo not set for doas" ); + is( $auth->{user}, $doas_user, 'merge_auth - doas user' ); + is( $auth->{password}, $doas_pass, 'merge_auth - doas password' ); + is( $auth->{doas}, TRUE(), 'merge_auth - doas enabled' ); + ok( !$auth->{sudo}, 'merge_auth - sudo not set for doas' ); } # Test global doas state Rex::global_doas(1); -ok( Rex::is_doas(), "global doas is enabled" ); +ok( Rex::is_doas(), 'global doas is enabled' ); Rex::global_doas(0); -ok( !Rex::is_doas(), "global doas is disabled" ); +ok( !Rex::is_doas(), 'global doas is disabled' ); # Test doas command function use Rex::Commands; # Test global doas toggle via doas command -doas "on"; -ok( Rex::is_doas(), "doas 'on' enables global doas" ); -doas "0"; -ok( !Rex::is_doas(), "doas '0' disables global doas" ); +doas 'on'; +ok( Rex::is_doas(), q{doas 'on' enables global doas} ); +doas '0'; +ok( !Rex::is_doas(), q{doas '0' disables global doas} ); # Test doas with code block my $doas_called = 0; doas sub { $doas_called = 1; Rex::is_doas(); }; -ok( $doas_called, "doas code block executed" ); +ok( $doas_called, 'doas code block executed' ); # Test doas with hashref options my $ret = doas { user => 'testuser', command => 'echo test' }; -ok( defined $ret, "doas with hashref returns result" ); +ok( defined $ret, 'doas with hashref returns result' ); From 3901aff23af8110029090a03d9e7ef786ecd73ec Mon Sep 17 00:00:00 2001 From: Rafael Medina Date: Sun, 27 Sep 2026 17:08:07 -0600 Subject: [PATCH 04/10] Extracting doas test from t0.31.t --- lib/Rex/CLI.pm | 2 +- lib/Rex/Interface/Exec/Doas.pm | 30 +++++++++---------- lib/Rex/Interface/File/Doas.pm | 2 +- lib/Rex/Interface/Fs/Doas.pm | 2 +- t/0.31.t | 53 +--------------------------------- t/doas.t | 44 ++++++++++++++++++++++++++++ 6 files changed, 63 insertions(+), 70 deletions(-) create mode 100644 t/doas.t diff --git a/lib/Rex/CLI.pm b/lib/Rex/CLI.pm index 52362d841..6e5203db9 100644 --- a/lib/Rex/CLI.pm +++ b/lib/Rex/CLI.pm @@ -446,7 +446,7 @@ sub __help__ { printf $fmt, "-O", "Pass additional options, like CMDB path"; printf $fmt, "-s", "Use sudo for every command"; printf $fmt, "-S", "Password for sudo"; - printf $fmt, "-D", "Use doas for every command"; + printf $fmt, "-D", 'Use doas for every command'; printf $fmt, "-t", "Number of threads to use (aka 'parallelism' param)"; printf $fmt, "-v", "Display (R)?ex version"; print "\n"; diff --git a/lib/Rex/Interface/Exec/Doas.pm b/lib/Rex/Interface/Exec/Doas.pm index 40b9b5788..42afe4c60 100644 --- a/lib/Rex/Interface/Exec/Doas.pm +++ b/lib/Rex/Interface/Exec/Doas.pm @@ -44,20 +44,20 @@ sub exec { my ( $exec, $file, $shell ); if ( my $ssh = Rex::is_ssh() ) { - if ( ref $ssh eq "Net::OpenSSH" ) { - $exec = Rex::Interface::Exec->create("OpenSSH"); - $file = Rex::Interface::File->create("OpenSSH"); + if ( ref $ssh eq 'Net::OpenSSH' ) { + $exec = Rex::Interface::Exec->create('OpenSSH'); + $file = Rex::Interface::File->create('OpenSSH'); } else { - $exec = Rex::Interface::Exec->create("SSH"); - $file = Rex::Interface::File->create("SSH"); + $exec = Rex::Interface::Exec->create('SSH'); + $file = Rex::Interface::File->create('SSH'); } } else { - $exec = Rex::Interface::Exec->create("Local"); - $file = Rex::Interface::File->create("Local"); + $exec = Rex::Interface::Exec->create('Local'); + $file = Rex::Interface::File->create('Local'); } - $shell = Rex::Interface::Shell->create("Sh"); + $shell = Rex::Interface::Shell->create('Sh'); my $doas_options = Rex::get_current_connection_object()->get_current_doas_options; @@ -68,7 +68,7 @@ sub exec { if ( Rex::Config->get_sudo_without_locales() ) { Rex::Logger::debug( - "Using doas without locales. If the locale is NOT C or en_US it will break many things!" + 'Using doas without locales. If the locale is NOT C or en_US it will break many things!' ); $option->{no_locales} = 1; } @@ -78,7 +78,7 @@ sub exec { if ( Rex::Config->get_sudo_without_sh() ) { Rex::Logger::debug( - "Using doas without sh will break things like file editing."); + 'Using doas without sh will break things like file editing'); $shell->set_inner_shell(0); $shell->set_doas_env(1); @@ -89,7 +89,7 @@ sub exec { } else { - $shell->set_locale("C"); + $shell->set_locale('C'); $shell->path($path); if ( Rex::Config->get_source_global_profile ) { @@ -120,15 +120,15 @@ sub _exec { my ( $exec, $file, $shell ); if ( my $ssh = Rex::is_ssh() ) { - if ( ref $ssh eq "Net::OpenSSH" ) { - $exec = Rex::Interface::Exec->create("OpenSSH"); + if ( ref $ssh eq 'Net::OpenSSH' ) { + $exec = Rex::Interface::Exec->create('OpenSSH'); } else { - $exec = Rex::Interface::Exec->create("SSH"); + $exec = Rex::Interface::Exec->create('SSH'); } } else { - $exec = Rex::Interface::Exec->create("Local"); + $exec = Rex::Interface::Exec->create('Local'); } return $exec->_exec( $cmd, $option ); diff --git a/lib/Rex/Interface/File/Doas.pm b/lib/Rex/Interface/File/Doas.pm index c58fd7f63..dcea609cb 100644 --- a/lib/Rex/Interface/File/Doas.pm +++ b/lib/Rex/Interface/File/Doas.pm @@ -13,7 +13,7 @@ our $VERSION = '9999.99.99_99'; # VERSION use base qw(Rex::Interface::File::Sudo); sub _fs { - return Rex::Interface::Fs->create("Doas"); + return Rex::Interface::Fs->create('Doas'); } 1; diff --git a/lib/Rex/Interface/Fs/Doas.pm b/lib/Rex/Interface/Fs/Doas.pm index 7bd815f47..2afbd1605 100644 --- a/lib/Rex/Interface/Fs/Doas.pm +++ b/lib/Rex/Interface/Fs/Doas.pm @@ -17,7 +17,7 @@ use base qw(Rex::Interface::Fs::Sudo); sub _exec { my ( $self, $cmd, $path, $option ) = @_; - my $exec = Rex::Interface::Exec->create("Doas"); + my $exec = Rex::Interface::Exec->create('Doas'); return $exec->exec( $cmd, $path, $option ); } diff --git a/t/0.31.t b/t/0.31.t index ac191d66b..5c31171d6 100755 --- a/t/0.31.t +++ b/t/0.31.t @@ -5,7 +5,7 @@ use warnings; our $VERSION = '9999.99.99_99'; # VERSION -use Test::More tests => 154; +use Test::More tests => 140; use Test::Warnings; use Rex -feature => '0.31'; @@ -153,54 +153,3 @@ is( get('key3')->{name}, 'foo', 'got value of name parameter in key3' ); is( get('key3')->{surname}, 'bar', 'got value of surname parameter in key3' ); is( get('key3')->{x1}, 'x', 'got value of NEW name parameter x1 in key3' ); is( get('key3')->{x2}, 'xx', 'got value of NEW name parameter x2 in key3' ); - -# doas tests -my $doas_group = 'doasgroup'; -my $doas_task = 'doastest1'; -my $doas_user = 'doasuser'; -my $doas_pass = 'doaspass'; - -group( $doas_group, 'doas01', 'doas02' ); -task( $doas_task, group => $doas_group, sub { } ); - -auth( - for => $doas_group, - user => $doas_user, - password => $doas_pass, - doas => TRUE() -); - -$task = Rex::TaskList->create()->get_task($doas_task); -@all_server = @{ $task->server }; - -for my $server (@all_server) { - my $auth = $task->merge_auth($server); - is( $auth->{user}, $doas_user, 'merge_auth - doas user' ); - is( $auth->{password}, $doas_pass, 'merge_auth - doas password' ); - is( $auth->{doas}, TRUE(), 'merge_auth - doas enabled' ); - ok( !$auth->{sudo}, 'merge_auth - sudo not set for doas' ); -} - -# Test global doas state -Rex::global_doas(1); -ok( Rex::is_doas(), 'global doas is enabled' ); -Rex::global_doas(0); -ok( !Rex::is_doas(), 'global doas is disabled' ); - -# Test doas command function -use Rex::Commands; - -# Test global doas toggle via doas command -doas 'on'; -ok( Rex::is_doas(), q{doas 'on' enables global doas} ); -doas '0'; -ok( !Rex::is_doas(), q{doas '0' disables global doas} ); - -# Test doas with code block -my $doas_called = 0; -doas sub { $doas_called = 1; Rex::is_doas(); }; -ok( $doas_called, 'doas code block executed' ); - -# Test doas with hashref options -my $ret = doas { user => 'testuser', command => 'echo test' }; -ok( defined $ret, 'doas with hashref returns result' ); diff --git a/t/doas.t b/t/doas.t new file mode 100644 index 000000000..09aec00ff --- /dev/null +++ b/t/doas.t @@ -0,0 +1,44 @@ +#!/usr/bin/env perl + +use v5.14.4; +use warnings; + +use Test::More tests => 7; +use Test::Warnings; + +use Rex -feature => '1.4'; +use Rex::Commands::Run; + +# Test global doas state +Rex::global_doas(1); +ok( Rex::is_doas(), 'global doas is enabled' ); + +Rex::global_doas(0); +ok( !Rex::is_doas(), 'global doas is disabled' ); + +# Test global doas toggle +doas 'on'; +ok( Rex::is_doas(), q{doas 'on' enables global doas} ); + +doas '0'; +ok( !Rex::is_doas(), q{doas '0' disables global doas} ); + +# Test doas with code block +my $doas_called = 0; + +doas sub { + $doas_called = 1; + Rex::is_doas(); +}; + +ok( $doas_called, 'doas code block executed' ); + +# Test doas with hashref options +my $ret = doas { + user => 'testuser', + command => 'echo test', +}; + +ok( defined $ret, 'doas with hashref returns result' ); + +done_testing; From fae061b3708c5ad245902a0d0e72de78b417227b Mon Sep 17 00:00:00 2001 From: Rafael Medina Date: Sun, 27 Sep 2026 17:28:43 -0600 Subject: [PATCH 05/10] Fixing perlcritic --- lib/Rex/Interface/Exec/Doas.pm | 18 +++++++++++------- lib/Rex/Interface/File/Doas.pm | 6 +++--- lib/Rex/Interface/Fs/Doas.pm | 10 +++++----- t/doas.t | 2 ++ 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/Rex/Interface/Exec/Doas.pm b/lib/Rex/Interface/Exec/Doas.pm index 42afe4c60..863355fe4 100644 --- a/lib/Rex/Interface/Exec/Doas.pm +++ b/lib/Rex/Interface/Exec/Doas.pm @@ -43,19 +43,23 @@ sub exec { } my ( $exec, $file, $shell ); + my $netOpenSSH = 'Net::OpenSSH' + my $openSSH = 'OpenSSH' + my $SSH = 'SSH' + my $local = 'Local' if ( my $ssh = Rex::is_ssh() ) { - if ( ref $ssh eq 'Net::OpenSSH' ) { - $exec = Rex::Interface::Exec->create('OpenSSH'); - $file = Rex::Interface::File->create('OpenSSH'); + if ( ref $ssh eq $netOpenSSH ) { + $exec = Rex::Interface::Exec->create($openSSH); + $file = Rex::Interface::File->create($openSSH); } else { - $exec = Rex::Interface::Exec->create('SSH'); - $file = Rex::Interface::File->create('SSH'); + $exec = Rex::Interface::Exec->create($SSH); + $file = Rex::Interface::File->create($SSH); } } else { - $exec = Rex::Interface::Exec->create('Local'); - $file = Rex::Interface::File->create('Local'); + $exec = Rex::Interface::Exec->create($local); + $file = Rex::Interface::File->create($local); } $shell = Rex::Interface::Shell->create('Sh'); diff --git a/lib/Rex/Interface/File/Doas.pm b/lib/Rex/Interface/File/Doas.pm index dcea609cb..97b243643 100644 --- a/lib/Rex/Interface/File/Doas.pm +++ b/lib/Rex/Interface/File/Doas.pm @@ -12,8 +12,8 @@ our $VERSION = '9999.99.99_99'; # VERSION use base qw(Rex::Interface::File::Sudo); -sub _fs { - return Rex::Interface::Fs->create('Doas'); -} +#sub _fs { +# return Rex::Interface::Fs->create('Doas'); +#} 1; diff --git a/lib/Rex/Interface/Fs/Doas.pm b/lib/Rex/Interface/Fs/Doas.pm index 2afbd1605..71b8afee6 100644 --- a/lib/Rex/Interface/Fs/Doas.pm +++ b/lib/Rex/Interface/Fs/Doas.pm @@ -15,10 +15,10 @@ use Rex::Interface::Fs::Sudo; use base qw(Rex::Interface::Fs::Sudo); -sub _exec { - my ( $self, $cmd, $path, $option ) = @_; - my $exec = Rex::Interface::Exec->create('Doas'); - return $exec->exec( $cmd, $path, $option ); -} +#sub _exec { +# my ( $self, $cmd, $path, $option ) = @_; +# my $exec = Rex::Interface::Exec->create('Doas'); +# return $exec->exec( $cmd, $path, $option ); +#} 1; diff --git a/t/doas.t b/t/doas.t index 09aec00ff..dc799d759 100644 --- a/t/doas.t +++ b/t/doas.t @@ -3,6 +3,8 @@ use v5.14.4; use warnings; +our $VERSION = '9999.99.99_99'; # VERSION + use Test::More tests => 7; use Test::Warnings; From 884e79b31b00b1e26df22456873d075fa6816bea Mon Sep 17 00:00:00 2001 From: Rafael Medina Date: Sun, 27 Sep 2026 18:09:37 -0600 Subject: [PATCH 06/10] Doas tified --- lib/Rex/Interface/Exec/Doas.pm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Rex/Interface/Exec/Doas.pm b/lib/Rex/Interface/Exec/Doas.pm index 863355fe4..c13f2a22a 100644 --- a/lib/Rex/Interface/Exec/Doas.pm +++ b/lib/Rex/Interface/Exec/Doas.pm @@ -43,10 +43,10 @@ sub exec { } my ( $exec, $file, $shell ); - my $netOpenSSH = 'Net::OpenSSH' - my $openSSH = 'OpenSSH' - my $SSH = 'SSH' - my $local = 'Local' + my $netOpenSSH = 'Net::OpenSSH'; + my $openSSH = 'OpenSSH'; + my $SSH = 'SSH'; + my $local = 'Local'; if ( my $ssh = Rex::is_ssh() ) { if ( ref $ssh eq $netOpenSSH ) { $exec = Rex::Interface::Exec->create($openSSH); From 85958f336c4bb392e90c61c3f0d45796ce739cf8 Mon Sep 17 00:00:00 2001 From: Rafael Medina Date: Sun, 27 Sep 2026 18:57:08 -0600 Subject: [PATCH 07/10] Polishing --- lib/Rex/Interface/Exec/Doas.pm | 46 ++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/lib/Rex/Interface/Exec/Doas.pm b/lib/Rex/Interface/Exec/Doas.pm index c13f2a22a..ebf71c487 100644 --- a/lib/Rex/Interface/Exec/Doas.pm +++ b/lib/Rex/Interface/Exec/Doas.pm @@ -21,21 +21,27 @@ use Rex::Helper::Path; use base 'Rex::Interface::Exec::Base'; +my $net_openssh = 'Net::OpenSSH'; +my $openssh = 'OpenSSH'; +my $ssh_backend = 'SSH'; +my $local = 'Local'; + sub new { - my $that = shift; - my $proto = ref($that) || $that; - my $self = {@_}; + my ( $that, @args ) = @_; + + my $self = {@args}; + my $proto = ref $that || $that; - bless( $self, $proto ); + bless $self, $proto; return $self; } -sub exec { +sub exec { ## no critic (Subroutines::ProhibitBuiltinHomonyms) my ( $self, $cmd, $path, $option ) = @_; if ( exists $option->{cwd} ) { - $cmd = "cd " . $option->{cwd} . " && $cmd"; + $cmd = 'cd ' . $option->{cwd} . " && $cmd"; } if ( exists $option->{path} ) { @@ -43,18 +49,14 @@ sub exec { } my ( $exec, $file, $shell ); - my $netOpenSSH = 'Net::OpenSSH'; - my $openSSH = 'OpenSSH'; - my $SSH = 'SSH'; - my $local = 'Local'; if ( my $ssh = Rex::is_ssh() ) { - if ( ref $ssh eq $netOpenSSH ) { - $exec = Rex::Interface::Exec->create($openSSH); - $file = Rex::Interface::File->create($openSSH); + if ( ref $ssh eq $net_openssh ) { + $exec = Rex::Interface::Exec->create($openssh); + $file = Rex::Interface::File->create($openssh); } else { - $exec = Rex::Interface::Exec->create($SSH); - $file = Rex::Interface::File->create($SSH); + $exec = Rex::Interface::Exec->create($ssh_backend); + $file = Rex::Interface::File->create($ssh_backend); } } else { @@ -65,9 +67,9 @@ sub exec { my $doas_options = Rex::get_current_connection_object()->get_current_doas_options; - my $doas_options_str = ""; + my $doas_options_str = q{}; if ( exists $doas_options->{user} ) { - $doas_options_str .= " -u " . $doas_options->{user}; + $doas_options_str .= ' -u ' . $doas_options->{user}; } if ( Rex::Config->get_sudo_without_locales() ) { @@ -119,20 +121,20 @@ sub exec { return $exec->direct_exec( $real_exec, $option ); } -sub _exec { +sub _exec { ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines) my ( $self, $cmd, $path, $option ) = @_; my ( $exec, $file, $shell ); if ( my $ssh = Rex::is_ssh() ) { - if ( ref $ssh eq 'Net::OpenSSH' ) { - $exec = Rex::Interface::Exec->create('OpenSSH'); + if ( ref $ssh eq $net_openssh ) { + $exec = Rex::Interface::Exec->create($openssh); } else { - $exec = Rex::Interface::Exec->create('SSH'); + $exec = Rex::Interface::Exec->create($ssh_backend); } } else { - $exec = Rex::Interface::Exec->create('Local'); + $exec = Rex::Interface::Exec->create($local); } return $exec->_exec( $cmd, $option ); From deb36f0fae8825b125cfbd5a33da46190ecd26cf Mon Sep 17 00:00:00 2001 From: Rafael Medina Date: Sun, 27 Sep 2026 19:44:49 -0600 Subject: [PATCH 08/10] More TestingAndDebugging --- lib/Rex/Interface/Connection/Base.pm | 23 ++++++++++++----------- lib/Rex/Interface/Shell/Base.pm | 3 ++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/Rex/Interface/Connection/Base.pm b/lib/Rex/Interface/Connection/Base.pm index c713affc8..f06927541 100644 --- a/lib/Rex/Interface/Connection/Base.pm +++ b/lib/Rex/Interface/Connection/Base.pm @@ -120,14 +120,14 @@ sub run_sudo_unmodified { sub push_doas_options { my ( $self, @option ) = @_; - if ( ref $option[0] eq "HASH" ) { - push @{ $self->{__doas_options__} }, $option[0]; - } - else { - push @{ $self->{__doas_options__} }, {@option}; - } -} + my $value = + ref $option[0] eq 'HASH' + ? $option[0] + : {@option}; + + return push @{ $self->{__doas_options__} }, $value; +} sub get_current_doas_options { my ($self) = @_; return $self->{__doas_options__}->[-1]; @@ -135,7 +135,8 @@ sub get_current_doas_options { sub push_use_doas { my ( $self, $use ) = @_; - push @{ $self->{__use_doas__} }, $use; + + return push @{ $self->{__use_doas__} }, $use; } sub get_current_use_doas { @@ -149,19 +150,19 @@ sub get_current_use_doas { sub pop_doas_options { my ($self) = @_; - pop @{ $self->{__doas_options__} }; + return pop @{ $self->{__doas_options__} }; } sub pop_use_doas { my ($self) = @_; - pop @{ $self->{__use_doas__} }; + return pop @{ $self->{__use_doas__} }; } sub run_doas_unmodified { my ( $self, $code ) = @_; $self->push_doas_options( {} ); $code->(); - $self->pop_doas_options(); + return $self->pop_doas_options(); } 1; diff --git a/lib/Rex/Interface/Shell/Base.pm b/lib/Rex/Interface/Shell/Base.pm index 55404210a..6d9f84095 100644 --- a/lib/Rex/Interface/Shell/Base.pm +++ b/lib/Rex/Interface/Shell/Base.pm @@ -49,7 +49,8 @@ sub set_sudo_env { sub set_doas_env { my ( $self, $doas_env ) = @_; - $self->{__doas_env__} = $doas_env; + + return $self->{__doas_env__} = $doas_env; } sub detect { From 6dbbe7ca6201baac241948e3f7b5dcfb883489a5 Mon Sep 17 00:00:00 2001 From: Rafael Medina Date: Sun, 27 Sep 2026 19:52:19 -0600 Subject: [PATCH 09/10] Tidying BASE --- lib/Rex/Interface/Connection/Base.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Rex/Interface/Connection/Base.pm b/lib/Rex/Interface/Connection/Base.pm index f06927541..175187c30 100644 --- a/lib/Rex/Interface/Connection/Base.pm +++ b/lib/Rex/Interface/Connection/Base.pm @@ -128,6 +128,7 @@ sub push_doas_options { return push @{ $self->{__doas_options__} }, $value; } + sub get_current_doas_options { my ($self) = @_; return $self->{__doas_options__}->[-1]; From 65b59b4d3b6d1bd2f095034c02bcfda5383c4a2f Mon Sep 17 00:00:00 2001 From: Rafael Medina Date: Sun, 27 Sep 2026 21:21:01 -0600 Subject: [PATCH 10/10] More cleaning and fixing --- lib/Rex.pm | 7 +++++-- lib/Rex/Commands/Run.pm | 26 +++++++++++++------------- lib/Rex/Interface/Connection/Base.pm | 26 +++++++++++++++++--------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/lib/Rex.pm b/lib/Rex.pm index a20484dc0..4ed18af2b 100644 --- a/lib/Rex.pm +++ b/lib/Rex.pm @@ -243,8 +243,10 @@ sub push_connection { } sub pop_connection { - pop @CONNECTION_STACK; - Rex::Logger::debug( "Connections in queue: " . scalar(@CONNECTION_STACK) ); + my $connection = pop @CONNECTION_STACK; + + Rex::Logger::debug( 'Connections in queue: ' . scalar @CONNECTION_STACK ); + return $connection; } sub reconnect_lost_connections { @@ -429,6 +431,7 @@ sub global_doas { # turn cache on Rex::Config->set_use_cache(1); + return 1; } =head2 get_sftp diff --git a/lib/Rex/Commands/Run.pm b/lib/Rex/Commands/Run.pm index 72edc1307..e86de5797 100644 --- a/lib/Rex/Commands/Run.pm +++ b/lib/Rex/Commands/Run.pm @@ -207,7 +207,7 @@ sub run { for my $_cmd ( @{$cmd} ) { &run( $_cmd, @_ ); } - return; + return undef; } my ( $code, $option ); @@ -246,7 +246,7 @@ sub run { } ); - return; + return undef; } if ( exists $option->{command} ) { @@ -467,12 +467,12 @@ sub sudo { if ( $cmd eq "on" || $cmd eq "-on" || $cmd eq "1" ) { Rex::Logger::debug("Turning sudo globally on"); Rex::global_sudo(1); - return; + return undef; } elsif ( $cmd eq "0" ) { Rex::Logger::debug("Turning sudo globally off"); Rex::global_sudo(0); - return; + return undef; } Rex::get_current_connection_object()->push_use_sudo(1); @@ -501,7 +501,7 @@ This function will execute the given command with doas. With this function you can run a command as another user via doas. B doas on OpenBSD does not support password input via stdin like sudo does. -You must configure /etc/doas.conf appropriately for unattended execution. +You must configure F appropriately for unattended execution. A typical configuration for a user to run commands as root without a password would be: permit nopass myuser as root @@ -535,20 +535,20 @@ sub doas { my ($cmd) = @_; my $options; - if ( ref $cmd eq "HASH" ) { + if ( ref $cmd eq 'HASH' ) { $options = $cmd; $cmd = $options->{command}; } - if ( $cmd eq "on" || $cmd eq "-on" || $cmd eq "1" ) { - Rex::Logger::debug("Turning doas globally on"); + if ( $cmd eq 'on' || $cmd eq '-on' || $cmd eq '1' ) { + Rex::Logger::debug('Turning doas globally on'); Rex::global_doas(1); - return; + return undef; } - elsif ( $cmd eq "0" ) { - Rex::Logger::debug("Turning doas globally off"); + elsif ( $cmd eq '0' ) { + Rex::Logger::debug('Turning doas globally off'); Rex::global_doas(0); - return; + return undef; } Rex::get_current_connection_object()->push_use_doas(1); @@ -557,7 +557,7 @@ sub doas { my $ret; # if doas is used with a code block - if ( ref($cmd) eq "CODE" ) { + if ( ref($cmd) eq 'CODE' ) { $ret = &$cmd(); } else { diff --git a/lib/Rex/Interface/Connection/Base.pm b/lib/Rex/Interface/Connection/Base.pm index 175187c30..7417e565b 100644 --- a/lib/Rex/Interface/Connection/Base.pm +++ b/lib/Rex/Interface/Connection/Base.pm @@ -12,6 +12,9 @@ our $VERSION = '9999.99.99_99'; # VERSION use Rex::Interface::Fs; use Rex::Interface::Exec; +my $hash_ref_type = 'HASH'; +my $last_index = -1; + sub new { my $that = shift; my $proto = ref($that) || $that; @@ -74,22 +77,24 @@ sub get_auth { sub push_sudo_options { my ( $self, @option ) = @_; - if ( ref $option[0] eq "HASH" ) { + if ( ref $option[0] eq $hash_ref_type ) { push @{ $self->{__sudo_options__} }, $option[0]; } else { push @{ $self->{__sudo_options__} }, {@option}; } + return $self->{__sudo_options__}; } sub get_current_sudo_options { my ($self) = @_; - return $self->{__sudo_options__}->[-1]; + return $self->{__sudo_options__}->[$last_index]; } sub push_use_sudo { my ( $self, $use ) = @_; push @{ $self->{__use_sudo__} }, $use; + return $self->{__use_sudo__}; } sub get_current_use_sudo { @@ -98,31 +103,34 @@ sub get_current_use_sudo { if ( $self->{is_sudo} ) { return 1; } - return $self->{__use_sudo__}->[-1]; + return $self->{__use_sudo__}->[$last_index]; } sub pop_sudo_options { my ($self) = @_; - pop @{ $self->{__sudo_options__} }; + my $popped = pop @{ $self->{__sudo_options__} }; + return $popped; } sub pop_use_sudo { my ($self) = @_; - pop @{ $self->{__use_sudo__} }; + my $popped = pop @{ $self->{__use_sudo__} }; + return $popped; } sub run_sudo_unmodified { my ( $self, $code ) = @_; $self->push_sudo_options( {} ); $code->(); - $self->pop_sudo_options(); + my $popped = $self->pop_sudo_options(); + return $popped; } sub push_doas_options { my ( $self, @option ) = @_; my $value = - ref $option[0] eq 'HASH' + ref $option[0] eq $hash_ref_type ? $option[0] : {@option}; @@ -131,7 +139,7 @@ sub push_doas_options { sub get_current_doas_options { my ($self) = @_; - return $self->{__doas_options__}->[-1]; + return $self->{__doas_options__}->[$last_index]; } sub push_use_doas { @@ -146,7 +154,7 @@ sub get_current_use_doas { if ( $self->{is_doas} ) { return 1; } - return $self->{__use_doas__}->[-1]; + return $self->{__use_doas__}->[$last_index]; } sub pop_doas_options {