#! /usr/bin/perl -T # vim: set filetype=perl ts=4 sw=4 sts=4 et: # NEEDGROUP osh-accountModify # SUDOERS # modify parameters/policy of an account # SUDOERS %osh-accountModify ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModify * # FILEMODE 0700 # FILEOWN 0 0 #>HEADER use common::sense; use Getopt::Long qw(:config no_auto_abbrev no_ignore_case); use File::Basename; use lib dirname(__FILE__) . '/../../lib/perl'; use OVH::Bastion; use OVH::Bastion::Helper; use OVH::Result; # Fetch command options my $fnret; my ($result, @optwarns); my ($account, @modify); eval { local $SIG{__WARN__} = sub { push @optwarns, shift }; $result = GetOptions( "account=s" => sub { $account //= $_[1] }, "modify=s" => \@modify, ); }; if ($@) { die $@ } if (!$result) { local $" = ", "; HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns"); } OVH::Bastion::Helper::check_spurious_args(); if (!$account || !@modify) { HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account' or 'modify'"); } #
PARAMS:ACCOUNT $fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account, localOnly => 1); $fnret or HEXIT($fnret); # get returned untainted value $account = $fnret->value->{'account'}; #RIGHTSCHECK if ($self eq 'root') { osh_debug "Real root, skipping checks of permissions"; } $fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountModify"); if (!$fnret) { HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self"); } if (OVH::Bastion::is_admin(account => $account, sudo => 1) && !OVH::Bastion::is_admin(account => $self, sudo => 1)) { HEXIT('ERR_SECURITY_VIOLATION', msg => "You can't modify the account of an admin without being admin yourself"); } #CODE my %result; # the TOTP and UNIX Password toggle codes are extremely similar, factorize it here sub _mfa_toggle { my ($key, $value, $mfaName, $mfaGroup, $mfaGroupBypass) = @_; my $jsonkey = $key; $jsonkey =~ s/-/_/g; # if the value is != bypass, remove the account from the bypass group if ($value ne 'bypass') { if (OVH::Bastion::is_user_in_group(user => $account, group => $mfaGroupBypass)) { $fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => $mfaGroupBypass, noisy_stderr => 1); if (!$fnret) { osh_warn "... error while removing the bypass option for this account"; $result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP'); return; } } } # if the value is == bypass, remove the account from the required group elsif ($value eq 'bypass') { if (OVH::Bastion::is_user_in_group(user => $account, group => $mfaGroup)) { $fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => $mfaGroup, noisy_stderr => 1); if (!$fnret) { osh_warn "... error while removing the required option for this account"; $result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP'); return; } } } $fnret = OVH::Bastion::is_user_in_group(user => $account, group => $mfaGroup); if ($value eq 'yes') { osh_info "Enforcing multi-factor authentication of type $mfaName for this account..."; if ($fnret) { osh_info "... no change was required"; $result{$jsonkey} = R('OK_NO_CHANGE'); return; } $fnret = OVH::Bastion::sys_addmembertogroup(user => $account, group => $mfaGroup, noisy_stderr => 1); if (!$fnret) { osh_warn "... error while setting the enforce option"; $result{$jsonkey} = R('ERR_ADDING_TO_GROUP'); return; } osh_info("... done, this account is now required to setup a password with --osh selfMFASetup$mfaName ", "on the next connection, before being allowed to do anything else"); $result{$jsonkey} = R('OK'); } elsif ($value eq 'no') { osh_info "Removing multi-factor authentication of type $mfaName requirement for this account..."; if (!$fnret) { osh_info "... no change was required"; $result{$jsonkey} = R('OK_NO_CHANGE'); return; } $fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => $mfaGroup, noisy_stderr => 1); if (!$fnret) { osh_warn "... error while setting the enforce option"; $result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP'); return; } osh_info( "... done, this account is no longer required to setup a password, however if there's already ", "a password configured, it'll still be required (if this is not expected, the password can be reset ", "with --osh accountMFAResetPassword command)" ); $result{$jsonkey} = R('OK'); } elsif ($value eq 'bypass') { osh_info "Bypassing multi-factor authentication of type $mfaName requirement for this account..."; $fnret = OVH::Bastion::is_user_in_group(user => $account, group => $mfaGroupBypass); if ($fnret) { osh_info "... no change was required"; $result{$jsonkey} = R('OK_NO_CHANGE'); return; } $fnret = OVH::Bastion::sys_addmembertogroup(user => $account, group => $mfaGroupBypass, noisy_stderr => 1); if (!$fnret) { osh_warn "... error while setting the enforce option"; $result{$jsonkey} = R('ERR_ADDING_TO_GROUP'); return; } osh_info( "... done, this account will no longer have to setup a password, even if this is enforced ", "by the default global policy.\n", "However if there's already a password configured, it'll still be required (if this is not expected, ", "the password can be reset with --osh accountMFAResetPassword command)" ); $result{$jsonkey} = R('OK'); } return; } sub _toggle_yes_no { my %params = @_; my $keyname = $params{'keyname'}; my $keyfile = $params{'keyfile'}; my $value = $params{'value'}; my $public = $params{'public'}; # by default, if public is not specified, it's 1 $public = 1 if !exists $params{'public'}; $fnret = OVH::Bastion::account_config(account => $account, public => $public, key => $keyfile); if ($value eq 'yes') { osh_info "Setting this account as $keyname..."; if ($fnret) { osh_info "... no change was required"; return R('OK_NO_CHANGE'); } $fnret = OVH::Bastion::account_config(account => $account, public => $public, key => $keyfile, value => 'yes'); if (!$fnret) { osh_warn "... error while setting the option"; return R('ERR_OPTION_CHANGE_FAILED'); } osh_info "... done, this account is now $keyname"; return R('OK'); } elsif ($value eq 'no') { osh_info "Removing the $keyname flag from this account..."; if (!$fnret) { osh_info "... no change was required"; return R('OK_NO_CHANGE'); } $fnret = OVH::Bastion::account_config(account => $account, public => $public, key => $keyfile, delete => 1); if (!$fnret) { osh_warn "... error while removing the option"; return R('ERR_OPTION_CHANGE_FAILED'); } osh_info "... done, this account has no longer the $keyname flag set"; return R('OK'); } else { return R('ERR_INVALID_PARAMETER', msg => "Invalid value passed to $keyfile"); } } foreach my $tuple (@modify) { my ($key, $value) = $tuple =~ /^([a-zA-Z0-9-]+)=([a-zA-Z0-9-]+)$/; next if (!$key || !defined $value); my $jsonkey = $key; $jsonkey =~ s/-/_/g; osh_debug "working on tuple key=$key value=$value"; if ($key eq 'always-active') { $result{$jsonkey} = _toggle_yes_no( value => $value, keyfile => OVH::Bastion::OPT_ACCOUNT_ALWAYS_ACTIVE, keyname => 'always-active' ); } elsif ($key eq 'idle-ignore') { $result{$jsonkey} = _toggle_yes_no(value => $value, keyfile => OVH::Bastion::OPT_ACCOUNT_IDLE_IGNORE, keyname => 'idle-ignore'); } elsif ($key eq 'osh-only') { $result{$jsonkey} = _toggle_yes_no( value => $value, public => 0, keyfile => OVH::Bastion::OPT_ACCOUNT_OSH_ONLY, keyname => 'osh-only' ); } elsif ($key eq 'pam-auth-bypass') { $fnret = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP); if ($value eq 'yes') { { osh_info "Bypassing sshd PAM auth usage for this account..."; if ($fnret) { osh_info "... no change was required"; $result{$jsonkey} = R('OK_NO_CHANGE'); last; } $fnret = OVH::Bastion::sys_addmembertogroup( user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP, noisy_stderr => 1 ); if (!$fnret) { osh_warn "... error while setting the bypass option"; $result{$jsonkey} = R('ERR_ADDING_TO_GROUP'); last; } osh_info "... done, this account will no longer use PAM for authentication"; $result{$jsonkey} = R('OK'); } } elsif ($value eq 'no') { { osh_info "Removing bypass of sshd PAM auth usage for this account..."; if (!$fnret) { osh_info "... no change was required"; $result{$jsonkey} = R('OK_NO_CHANGE'); last; } $fnret = OVH::Bastion::sys_delmemberfromgroup( user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP, noisy_stderr => 1 ); if (!$fnret) { osh_warn "... error while removing the bypass option"; $result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP'); last; } osh_info "... done, this account will no longer bypass PAM for authentication"; $result{$jsonkey} = R('OK'); } } } elsif ($key eq 'pubkey-auth-optional') { $fnret = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::OSH_PUBKEY_AUTH_OPTIONAL_GROUP); if ($value eq 'yes') { { osh_info "Making public key authentication optional for this account..."; if ($fnret) { osh_info "... no change was required"; $result{$jsonkey} = R('OK_NO_CHANGE'); last; } $fnret = OVH::Bastion::sys_addmembertogroup( user => $account, group => OVH::Bastion::OSH_PUBKEY_AUTH_OPTIONAL_GROUP, noisy_stderr => 1 ); if (!$fnret) { osh_warn "... error while setting the optional pubkey option"; $result{$jsonkey} = R('ERR_ADDING_TO_GROUP'); last; } osh_info("... done, this account can now authenticate with or without a pubkey ", "if a password/TOTP is set"); $result{$jsonkey} = R('OK'); } } elsif ($value eq 'no') { { osh_info "Making pubkey authentication mandatory for this account..."; if (!$fnret) { osh_info "... no change was required"; $result{$jsonkey} = R('OK_NO_CHANGE'); last; } $fnret = OVH::Bastion::sys_delmemberfromgroup( user => $account, group => OVH::Bastion::OSH_PUBKEY_AUTH_OPTIONAL_GROUP, noisy_stderr => 1 ); if (!$fnret) { osh_warn "... error while removing the optional pubkey option"; $result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP'); last; } osh_info "... done, this account now requires a pubkey to authenticate"; $result{$jsonkey} = R('OK'); } } } elsif ($key eq 'mfa-password-required') { _mfa_toggle( $key, $value, 'Password', OVH::Bastion::MFA_PASSWORD_REQUIRED_GROUP, OVH::Bastion::MFA_PASSWORD_BYPASS_GROUP ); } elsif ($key eq 'mfa-totp-required') { _mfa_toggle($key, $value, 'TOTP', OVH::Bastion::MFA_TOTP_REQUIRED_GROUP, OVH::Bastion::MFA_TOTP_BYPASS_GROUP); } elsif ($key eq 'egress-strict-host-key-checking') { osh_info "Changing the egress StrictHostKeyChecking option for this account..."; if (not grep { $value eq $_ } qw{ yes accept-new no ask default bypass }) { osh_warn "Invalid parameter '$value', skipping"; $result{$jsonkey} = R('ERR_INVALID_PARAMETER'); } else { my $hostsFile; # undef, aka remove UserKnownHostsFile option if ($value eq 'bypass') { # special case: for 'bypass', we set Strict to no and UserKnownHostsFile to /dev/null $value = 'no'; $hostsFile = '/dev/null'; } elsif ($value eq 'default') { # special case: for 'default', we actually remove the StrictHostKeyChecking option undef $value; } $fnret = OVH::Bastion::account_ssh_config_set( account => $account, key => "StrictHostKeyChecking", value => $value ); $result{$jsonkey} = $fnret; if ($fnret) { $fnret = OVH::Bastion::account_ssh_config_set( account => $account, key => "UserKnownHostsFile", value => $hostsFile ); $result{$jsonkey} = $fnret; } if ($fnret) { osh_info "... modification done"; } else { osh_warn "... error while setting StrictHostKeyChecking policy: " . $fnret->msg; } } } elsif ($key eq 'personal-egress-mfa-required') { osh_info "Changing the MFA policy for egress connections using the personal access (and keys) of the account..."; if (not grep { $value eq $_ } qw{ password totp any none }) { osh_warn "Invalid parameter '$value', skipping"; $result{$jsonkey} = R('ERR_INVALID_PARAMETER'); } else { $fnret = OVH::Bastion::account_config(account => $account, key => "personal_egress_mfa_required", value => $value); $result{$jsonkey} = $fnret; if ($fnret) { osh_info "... modification done"; } else { osh_warn "... error while setting MFA policy: " . $fnret->msg; } } } elsif ($key eq 'max-inactive-days') { osh_info "Changing the account expiration policy..."; if ($value !~ /^(?:\d+|-1)$/) { osh_warn "Invalid parameter '$value', skipping"; $result{$jsonkey} = R('ERR_INVALID_PARAMETER'); } else { my %todo = ($value >= 0 ? (value => $value) : (delete => 1)); $fnret = OVH::Bastion::account_config( account => $account, %todo, %{OVH::Bastion::OPT_ACCOUNT_MAX_INACTIVE_DAYS()} ); $result{$jsonkey} = $fnret; if ($fnret) { osh_info "... modification done"; } else { osh_warn "... error while setting the account expiration policy: " . $fnret->msg; } } } } HEXIT('OK', value => \%result);