From aaaa173764c5c81e8ce6c2e9ae58a125ca133a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Wed, 15 Dec 2021 15:50:39 +0000 Subject: [PATCH] feat: add the accountUnlock restricted plugin --- bin/helper/osh-accountUnlock | 102 ++++++++++++++++++ bin/plugin/restricted/accountUnlock | 48 +++++++++ .../plugins/restricted/accountUnlock.rst | 20 ++++ doc/sphinx/plugins/restricted/index.rst | 1 + etc/sudoers.d/osh-plugin-accountUnlock | 1 + lib/perl/OVH/Bastion.pm | 2 +- lib/perl/OVH/Bastion/os.inc | 10 ++ tests/functional/tests.d/325-accountinfo.sh | 15 +++ 8 files changed, 198 insertions(+), 1 deletion(-) create mode 100755 bin/helper/osh-accountUnlock create mode 100755 bin/plugin/restricted/accountUnlock create mode 100644 doc/sphinx/plugins/restricted/accountUnlock.rst create mode 100644 etc/sudoers.d/osh-plugin-accountUnlock diff --git a/bin/helper/osh-accountUnlock b/bin/helper/osh-accountUnlock new file mode 100755 index 0000000..99bf212 --- /dev/null +++ b/bin/helper/osh-accountUnlock @@ -0,0 +1,102 @@ +#! /usr/bin/perl -T +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +# NEEDGROUP osh-accountUnlock +# SUDOERS %osh-accountUnlock ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountUnlock * +# FILEMODE 0700 +# FILEOWN 0 0 + +#>HEADER +use common::sense; +use Getopt::Long; +use File::Basename; +use lib dirname(__FILE__) . '/../../lib/perl'; +use OVH::Result; +use OVH::Bastion; +use OVH::Bastion::Helper; + +# Fetch command options +my $fnret; +my ($result, @optwarns); +my ($account); +eval { + local $SIG{__WARN__} = sub { push @optwarns, shift }; + $result = GetOptions("account=s" => sub { $account //= $_[1] },); +}; +if ($@) { die $@ } + +if (!$result) { + local $" = ", "; + HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns"); +} +if (!$account) { + HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account'"); +} + +#
RIGHTSCHECK +if ($self eq 'root') { + osh_debug "Real root, skipping checks of permissions"; +} +else { + # need to perform another security check + $fnret = OVH::Bastion::is_user_in_group( + user => $self, + group => "osh-accountUnlock" + ); + if (!$fnret) { + HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self"); + } +} + +#PARAMS:ACCOUNT +osh_debug("Checking account"); +$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account); +$fnret or HEXIT($fnret); +$account = $fnret->value->{'account'}; # untainted + +# [qw{ faillock --user }, $account], + pam_tally2 => [qw{ pam_tally2 -u }, $account, '-r'], + pam_tally => [qw{ pam_tally --user }, $account, '--reset'], +); + +my $found; +foreach my $program (@programs) { + next if not $cmds{$program}; + next if not OVH::Bastion::is_in_path(binary => $cmds{$program}[0]); + $found = $program; + last; +} + +if (!$found) { + if (OVH::Bastion::is_linux()) { + warn_syslog("Couldn't unlock account $account, as neither faillock, pam_tally2 or pam_tally seem to be installed"); + HEXIT('ERR_HELPER_MISSING', msg => "Found no unlock helper on this system. Please contact your sysadmin!"); + } + else { + HEXIT('ERR_UNSUPPORTED_FEATURE', msg => "Can't unlock account, your system might not support it"); + } +} + +$fnret = OVH::Bastion::execute(cmd => $cmds{$found}, must_succeed => 1, noisy_stdout => 1, noisy_stderr => 1); +if (!$fnret) { + my $error = '(empty)'; + if ($fnret->value->{'stderr'}) { + $error = $fnret->value->{'stderr'}[0]; + } + elsif ($fnret->value->{'stdout'}) { + $error = $fnret->value->{'stdout'}[0]; + } + warn_syslog("Got an error trying to unlock account $account through $found, first returned line was '$error'"); + HEXIT R('ERR_INTERNAL', msg => "Failed to unlock $account"); +} + +HEXIT('OK', value => {account => $account}, msg => "Account '$account' has been successfully unlocked."); diff --git a/bin/plugin/restricted/accountUnlock b/bin/plugin/restricted/accountUnlock new file mode 100755 index 0000000..0c72bf3 --- /dev/null +++ b/bin/plugin/restricted/accountUnlock @@ -0,0 +1,48 @@ +#! /usr/bin/env perl +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +use common::sense; +use Term::ANSIColor qw{ colored }; +use POSIX qw{ strftime }; +use File::Basename; +use lib dirname(__FILE__) . '/../../../lib/perl'; +use OVH::Result; +use OVH::Bastion; +use OVH::Bastion::Plugin qw( :DEFAULT help ); +my ($account); +OVH::Bastion::Plugin::begin( + argv => \@ARGV, + header => "unlock an account", + options => { + "account=s" => \$account + }, + helptext => <<'EOF', +Unlock an account locked by pam_tally, pam_tally2 or pam_faillock + +Usage: --osh SCRIPT_NAME --account ACCOUNT + + --account ACCOUNT Account to work on +EOF +); +# +# code +# +my $fnret; +if (not $account) { + help(); + osh_exit 'ERR_MISSING_PARAMETER', "Missing 'account' parameter"; +} + +# Here we parse account name +$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account); +$fnret + or osh_exit $fnret; +$account = $fnret->value->{'account'}; +my @command = qw{ sudo -n -u root -- }; +push @command, qw{ /usr/bin/env perl -T }; +push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-accountUnlock'; +push @command, ('--account', $account); +osh_exit( + OVH::Bastion::helper( + cmd => \@command + ) +); diff --git a/doc/sphinx/plugins/restricted/accountUnlock.rst b/doc/sphinx/plugins/restricted/accountUnlock.rst new file mode 100644 index 0000000..0f25efe --- /dev/null +++ b/doc/sphinx/plugins/restricted/accountUnlock.rst @@ -0,0 +1,20 @@ +============== +accountUnlock +============== + +Unlock an account locked by pam_tally, pam_tally2 or pam_faillock +================================================================= + + +.. admonition:: usage + :class: cmdusage + + --osh accountUnlock --account ACCOUNT + +.. program:: accountUnlock + + +.. option:: --account ACCOUNT + + Account to work on + diff --git a/doc/sphinx/plugins/restricted/index.rst b/doc/sphinx/plugins/restricted/index.rst index 6588e48..6634efa 100644 --- a/doc/sphinx/plugins/restricted/index.rst +++ b/doc/sphinx/plugins/restricted/index.rst @@ -22,6 +22,7 @@ restricted plugins accountPIV accountRevokeCommand accountUnexpire + accountUnlock groupCreate groupDelete realmCreate diff --git a/etc/sudoers.d/osh-plugin-accountUnlock b/etc/sudoers.d/osh-plugin-accountUnlock new file mode 100644 index 0000000..1eb2987 --- /dev/null +++ b/etc/sudoers.d/osh-plugin-accountUnlock @@ -0,0 +1 @@ +%osh-accountUnlock ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountUnlock * diff --git a/lib/perl/OVH/Bastion.pm b/lib/perl/OVH/Bastion.pm index 1d8ed55..e8fe8b5 100644 --- a/lib/perl/OVH/Bastion.pm +++ b/lib/perl/OVH/Bastion.pm @@ -141,7 +141,7 @@ my %_autoload_files = ( qw{ enable_mocking is_mocking set_mock_data mock_get_account_entry mock_get_account_accesses mock_get_account_personal_accesses mock_get_account_legacy_accesses mock_get_group_accesses mock_get_account_guest_accesses } ], os => [ - qw{ sysinfo is_linux is_debian is_redhat is_bsd is_freebsd is_openbsd is_netbsd has_acls sys_useradd sys_groupadd sys_userdel sys_groupdel sys_addmembertogroup sys_delmemberfromgroup sys_changepassword sys_neutralizepassword sys_setpasswordpolicy sys_getpasswordinfo sys_getsudoersfolder sys_setfacl } + qw{ sysinfo is_linux is_debian is_redhat is_bsd is_freebsd is_openbsd is_netbsd has_acls sys_useradd sys_groupadd sys_userdel sys_groupdel sys_addmembertogroup sys_delmemberfromgroup sys_changepassword sys_neutralizepassword sys_setpasswordpolicy sys_getpasswordinfo sys_getsudoersfolder sys_setfacl is_in_path } ], password => [qw{ get_hashes_from_password get_hashes_list is_valid_hash }], ssh => [ diff --git a/lib/perl/OVH/Bastion/os.inc b/lib/perl/OVH/Bastion/os.inc index f3b908f..e8c3078 100644 --- a/lib/perl/OVH/Bastion/os.inc +++ b/lib/perl/OVH/Bastion/os.inc @@ -586,4 +586,14 @@ sub sys_setfacl { return R('OK'); } +sub is_in_path { + my %params = @_; + my $binary = $params{'binary'}; + + foreach my $path (split /:/, $ENV{'PATH'}) { + return R('OK', value => "$path/$binary") if -f -x "$path/$binary"; + } + return R('KO', msg => "$binary was not found in PATH"); +} + 1; diff --git a/tests/functional/tests.d/325-accountinfo.sh b/tests/functional/tests.d/325-accountinfo.sh index 3687bca..7fc981b 100644 --- a/tests/functional/tests.d/325-accountinfo.sh +++ b/tests/functional/tests.d/325-accountinfo.sh @@ -55,6 +55,21 @@ testsuite_accountinfo() json .value.already_seen_before 1 contain "Last seen on" + # try to unlock + grant accountUnlock + + run a0_unlock_a1 $a0 --osh accountUnlock --account $account1 + json .command accountUnlock + if [ "$OS_FAMILY" = Linux ]; then + retvalshouldbe 0 + json .error_code OK + else + retvalshouldbe 100 + json .error_code ERR_UNSUPPORTED_FEATURE + fi + + revoke accountUnlock + grant accountModify # a0 changes a2 expiration policy