Skip to content

Commit

Permalink
feat: add groupSetServers
Browse files Browse the repository at this point in the history
  • Loading branch information
speed47 committed Aug 8, 2024
1 parent 3d2cf21 commit c2b1e5f
Show file tree
Hide file tree
Showing 7 changed files with 461 additions and 45 deletions.
105 changes: 105 additions & 0 deletions bin/helper/osh-groupSetServers
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# KEYSUDOERS # as an aclkeeper, we can add/del a server from the group server list in /home/%GROUP%/allowed.ip
# KEYSUDOERS SUPEROWNERS, %%GROUP%-aclkeeper ALL=(%GROUP%) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupSetServers --group %GROUP%
# FILEMODE 0755
# FILEOWN 0 0

#>HEADER
use common::sense;
use Getopt::Long qw(:config no_auto_abbrev no_ignore_case);
use JSON;

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 $group;
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"group=s" => sub { $group //= $_[1] }, # ignore subsequent --group on cmdline (anti-sudoers-override)
);
};
if ($@) { die $@ }

if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}

OVH::Bastion::Helper::check_spurious_args();

if (not $group) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'group'");
}

#<HEADER

#>PARAMS:GROUP
osh_debug("Checking group $group");
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => 'key');
$fnret or HEXIT($fnret);

# get returned untainted value
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};
osh_debug("got group $group/$shortGroup");

#<PARAMS:GROUP

#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
$fnret = OVH::Bastion::is_group_aclkeeper(account => $self, group => $shortGroup, sudo => 1, superowner => 1);
$fnret or HEXIT('ERR_NOT_ALLOWED', msg => "Sorry, you must be an aclkeeper of group $shortGroup");
}

#<RIGHTSCHECK

#>CODE

# the new ACL is built by the plugin and sent to our STDIN in pre-parsed JSON format
my $jsonData = <STDIN>;
my $data = eval { decode_json($jsonData); };
if ($@) {
HEXIT('ERR_INVALID_ARGUMENT', msg => "Invalid JSON data sent by the plugin, couldn't decode");
}

if (!$data || ref $data ne 'ARRAY') {
HEXIT('ERR_INVALID_ARGUMENT', msg => "Invalid JSON import format sent by the plugin");
}

$fnret = OVH::Bastion::access_modify(
way => 'group',
action => 'clear',
group => $group,
);
$fnret or HEXIT($fnret);

osh_info("Setting ACL entries, this may take a while...");

my @errors;
foreach my $entry (@$data) {
$fnret = OVH::Bastion::access_modify(
way => 'group',
action => 'add',
group => $group,
ip => $entry->{ip},
user => $entry->{user},
port => $entry->{port},
);
push @errors, $fnret if !$fnret;
}

if (!@errors) {
HEXIT('OK', value => {ACL => $data, errors => []});
}
HEXIT('OK_WITH_ERRORS', value => {ACL => $data, errors => \@errors});
188 changes: 188 additions & 0 deletions bin/plugin/group-aclkeeper/groupSetServers
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use JSON;

use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
use OVH::Bastion::Plugin::ACL;

my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "replace a group's current ACL by a new one",
userAllowWildcards => 1,
options => {
"group=s" => \my $group,
"dry-run" => \my $dryRun,
"skip-errors" => \my $skipErrors,
},
helptext => <<'EOF',
Replace a group's current ACL by a new list
Usage: --osh SCRIPT_NAME --group GROUP [OPTIONS]
--group GROUP Specify which group to modify the ACL of
--dry-run Don't actually modify the ACL, just report whether the input contains errors
--skip-errors Don't abort on STDIN parsing errors, just skip the non-parseable lines
The list of the assets to constitute the new ACL should then be given on ``STDIN``,
respecting the following format: ``[USER@]HOST[:PORT][ COMMENT]``, with ``USER`` and ``PORT`` being optional,
and ``HOST`` being either a hostname, an IP, or an IP block in CIDR notation. The ``COMMENT`` is also optional,
and may contain spaces.
Example of valid lines to be fed through ``STDIN``::
server12.example.org
logs@server
192.0.2.21
host1.example.net:2222 host1 on secondary sshd with alternate port
[email protected]/24 production database cluster
EOF
);

my $fnret;

if (not $group) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "Missing mandatory parameter 'group'";
}

$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key");
$fnret or osh_exit($fnret);

# get returned untainted value
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};

$fnret = OVH::Bastion::is_group_aclkeeper(account => $self, group => $shortGroup, superowner => 1);
$fnret
or osh_exit 'ERR_NOT_GROUP_ACLKEEPER',
"Sorry, you must be an aclkeeper of group $shortGroup to be able to add servers to it";

osh_info
"Specify the entries of the new ACL below, one per line, with the following format: [USER\@]HOST[:PORT][ COMMENT]";
osh_info "The list ends at EOF (usually CTRL+D).";
osh_info "You may abort with CTRL+C if needed.";

my @ACL;
my @errors;
my $nbLines = 0;
my $comment;
while (my $line = <STDIN>) {
# trim white spaces
$line =~ s/^\s+|\s+$//g;

# empty line ?
$line or next;

$nbLines++;

my ($acl_user, $acl_host, $acl_ip, $acl_port);
if ($line =~ m{^(?:(\S+)\@)?([a-zA-Z0-9_./-]+)(?::(\d+))?(?:\s+(.+))?$}) {
$acl_user = $1;
$acl_host = $2;
$acl_port = $3;
$comment = $4;
}
else {
push @errors, "Couldn't parse the line '$line'";
osh_warn($errors[-1]);
next;
}

# check port
if (defined $acl_port) {
$fnret = OVH::Bastion::is_valid_port(port => $acl_port);
if (!$fnret) {
push @errors, "In line $nbLines ($line), port '$acl_port' is invalid";
osh_warn($errors[-1]);
next;
}
$acl_port = $fnret->value;
}

# check user
if (defined $acl_user) {
$fnret = OVH::Bastion::is_valid_remote_user(user => $acl_user, allowWildcards => 1);
if (!$fnret) {
push @errors, "In line $nbLines ($line), user '$acl_user' is invalid";
osh_warn($errors[-1]);
next;
}
$acl_user = $fnret->value;
}

# resolve host, unless it looks like a prefix
if ($acl_host =~ m{/}) {
$fnret = OVH::Bastion::is_valid_ip(ip => $acl_host, allowPrefixes => 1);
}
else {
$fnret = OVH::Bastion::get_ip(host => $acl_host);
}
if (!$fnret) {
push @errors, "In line $nbLines ($line), $fnret";
osh_warn($errors[-1]);
next;
}
else {
$acl_ip = $fnret->value->{'ip'};
}

push @ACL, {ip => $acl_ip, port => $acl_port, user => $acl_user, comment => $comment};
}

osh_info("Parsed " . @ACL . "/$nbLines lines successfully");

if (@errors && !$skipErrors) {
osh_exit(
R(
'ERR_INVALID_PARAMETER',
msg => "Aborting due to the "
. @errors
. " parsing or host resolving errors above, use --skip-errors to proceed anyway",
value => {parsedLines => $nbLines, dryrun => $dryRun ? \1 : \0, errors => \@errors, ACL => \@ACL},
)
);
}

if ($dryRun) {
osh_ok({parsedLines => $nbLines, errors => \@errors, dryrun => \1, ACL => \@ACL});
}

#
# Now do it
#

if (!@ACL) {
osh_exit(R('OK_NO_CHANGE', msg => "No ACL was given, no change was made"));
}

my @command = qw{ sudo -n -u };
push @command,
($group, '--', '/usr/bin/env', 'perl', '-T', $OVH::Bastion::BASEPATH . '/bin/helper/osh-groupSetServers');
push @command, '--group', $group;

$fnret = OVH::Bastion::helper(cmd => \@command, stdin_str => encode_json(\@ACL));
$fnret or osh_exit($fnret);

# merge both error lists
if ($fnret->value && $fnret->value->{'errors'}) {
push @errors, @{$fnret->value->{'errors'} || []};
}

osh_exit(
R(
'OK',
msg => "The new ACL has been set with " . @{$fnret->value->{'ACL'}} . " entries and " . @errors . " errors",
value => {
parsedLines => $nbLines,
dryrun => $dryRun ? \1 : \0,
group => $shortGroup,
ACL => $fnret->value->{'ACL'},
errors => \@errors
}
)
);
40 changes: 40 additions & 0 deletions doc/sphinx/plugins/group-aclkeeper/groupSetServers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
================
groupSetServers
================

Replace a group's current ACL by a new list
===========================================


.. admonition:: usage
:class: cmdusage

--osh groupSetServers --group GROUP [OPTIONS]

.. program:: groupSetServers


.. option:: --group GROUP

Specify which group to modify the ACL of

.. option:: --dry-run

Don't actually modify the ACL, just report whether the input contains errors

.. option:: --skip-errors

Don't abort on STDIN parsing errors, just skip the non-parseable lines


The list of the assets to constitute the new ACL should then be given on ``STDIN``,
respecting the following format: ``[USER@]HOST[:PORT][ COMMENT]``, with ``USER`` and ``PORT`` being optional,
and ``HOST`` being either a hostname, an IP, or an IP block in CIDR notation. The ``COMMENT`` is also optional,
and may contain spaces.

Example of valid lines to be fed through ``STDIN``::

server12.example.org
logs@server
host1.example.net:2222 host1 on secondary sshd with alternate port
[email protected]/24 production database cluster
1 change: 1 addition & 0 deletions doc/sphinx/plugins/group-aclkeeper/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ group-aclkeeper plugins

groupAddServer
groupDelServer
groupSetServers
3 changes: 3 additions & 0 deletions etc/sudoers.group.template.d/500-base.sudoers
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@ SUPEROWNERS, %%GROUP%-gatekeeper ALL=(allowkeeper) NOPASSWD: /usr/bin/env perl -
# as an aclkeeper, we can add/del a server from the group server list in /home/%GROUP%/allowed.ip
SUPEROWNERS, %%GROUP%-aclkeeper ALL=(%GROUP%) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupAddServer --group %GROUP% *

# as an aclkeeper, we can replace the group servers list in /home/%GROUP%/allowed.ip in batch with one command
SUPEROWNERS, %%GROUP%-aclkeeper ALL=(%GROUP%) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupSetServers --group %GROUP%

# as an owner, we can delete our own group
SUPEROWNERS, %%GROUP%-owner ALL=(root) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupDelete --group %GROUP%
Loading

0 comments on commit c2b1e5f

Please sign in to comment.