Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add groupSetServers #490

Merged
merged 3 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/freebsd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ jobs:
# to do proper tests, we need the fs to have ACLs enabled
sudo mount -o acls /
# install required packages
sudo pkg update
sudo pkg install -y bash rsync ca_root_nss jq fping screen flock curl
sudo env IGNORE_OSVERSION=yes pkg update
sudo env IGNORE_OSVERSION=yes pkg install -y bash rsync ca_root_nss jq fping screen flock curl
# create required folder
sudo mkdir -p /opt/bastion
# copy bastion code to the proper location
Expand Down
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
}
)
);
41 changes: 41 additions & 0 deletions doc/sphinx/plugins/group-aclkeeper/groupSetServers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
================
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
192.0.2.21
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