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..4ed18af2b 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 = (); @@ -241,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 { @@ -385,6 +389,51 @@ 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); + return 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..6e5203db9 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..e86de5797 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) @@ -206,7 +207,7 @@ sub run { for my $_cmd ( @{$cmd} ) { &run( $_cmd, @_ ); } - return; + return undef; } my ( $code, $option ); @@ -245,7 +246,7 @@ sub run { } ); - return; + return undef; } if ( exists $option->{command} ) { @@ -466,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); @@ -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 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 + +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 undef; + } + elsif ( $cmd eq '0' ) { + Rex::Logger::debug('Turning doas globally off'); + Rex::global_doas(0); + return undef; + } + + 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..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; @@ -20,6 +23,7 @@ sub new { bless( $self, $proto ); $self->{__sudo_options__} = []; + $self->{__doas_options__} = []; return $self; } @@ -73,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 { @@ -97,24 +103,75 @@ 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_type + ? $option[0] + : {@option}; + + return push @{ $self->{__doas_options__} }, $value; +} + +sub get_current_doas_options { + my ($self) = @_; + return $self->{__doas_options__}->[$last_index]; +} + +sub push_use_doas { + my ( $self, $use ) = @_; + + return push @{ $self->{__use_doas__} }, $use; +} + +sub get_current_use_doas { + my ($self) = @_; + + if ( $self->{is_doas} ) { + return 1; + } + return $self->{__use_doas__}->[$last_index]; +} + +sub pop_doas_options { + my ($self) = @_; + return pop @{ $self->{__doas_options__} }; +} + +sub pop_use_doas { + my ($self) = @_; + return pop @{ $self->{__use_doas__} }; +} + +sub run_doas_unmodified { + my ( $self, $code ) = @_; + $self->push_doas_options( {} ); + $code->(); + return $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..ebf71c487 --- /dev/null +++ b/lib/Rex/Interface/Exec/Doas.pm @@ -0,0 +1,143 @@ +# +# (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'; + +my $net_openssh = 'Net::OpenSSH'; +my $openssh = 'OpenSSH'; +my $ssh_backend = 'SSH'; +my $local = 'Local'; + +sub new { + my ( $that, @args ) = @_; + + my $self = {@args}; + my $proto = ref $that || $that; + + bless $self, $proto; + + return $self; +} + +sub exec { ## no critic (Subroutines::ProhibitBuiltinHomonyms) + 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_backend); + $file = Rex::Interface::File->create($ssh_backend); + } + } + 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 = q{}; + 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 { ## 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); + } + else { + $exec = Rex::Interface::Exec->create($ssh_backend); + } + } + 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..97b243643 --- /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..71b8afee6 --- /dev/null +++ b/lib/Rex/Interface/Fs/Doas.pm @@ -0,0 +1,24 @@ +# +# (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 + +use Rex::Interface::Exec; +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 ); +#} + +1; diff --git a/lib/Rex/Interface/Shell/Base.pm b/lib/Rex/Interface/Shell/Base.pm index fe11c7176..6d9f84095 100644 --- a/lib/Rex/Interface/Shell/Base.pm +++ b/lib/Rex/Interface/Shell/Base.pm @@ -47,6 +47,12 @@ sub set_sudo_env { $self->{__sudo_env__} = $sudo_env; } +sub set_doas_env { + my ( $self, $doas_env ) = @_; + + return $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..5c31171d6 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,30 +126,30 @@ 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", "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/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", { 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( 'key1', "val1" ); +is( get('key1'), "val1", "got 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/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', { 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' ); diff --git a/t/doas.t b/t/doas.t new file mode 100644 index 000000000..dc799d759 --- /dev/null +++ b/t/doas.t @@ -0,0 +1,46 @@ +#!/usr/bin/env perl + +use v5.14.4; +use warnings; + +our $VERSION = '9999.99.99_99'; # VERSION + +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;