mirror of
https://github.com/ovh/the-bastion.git
synced 2024-12-28 11:13:10 +08:00
443 lines
16 KiB
Perl
Executable file
443 lines
16 KiB
Perl
Executable file
#! /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'");
|
|
}
|
|
|
|
#<HEADER
|
|
|
|
#>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'};
|
|
|
|
#<PARAMS: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");
|
|
}
|
|
|
|
#<RIGHTSCHECK
|
|
|
|
#>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);
|