\!/ KyuuKazami \!/

Path : /scripts/
Upload :
Current File : //scripts/fix_reseller_acls

#!/usr/local/cpanel/3rdparty/bin/perl
package scripts::fix_reseller_acls;

# cpanel - scripts/fix_reseller_acls
#                                                    Copyright 2017 cPanel, Inc.
#                                                           All rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

use strict;
use warnings;

use Try::Tiny;
use Pod::Usage   ();
use Getopt::Long ();

use Cpanel::Exception    ();
use Cpanel::LoadModule   ();
use Cpanel::ConfigFiles  ();
use Whostmgr::ACLS::Data ();

exit run(@ARGV) unless caller();

sub run {
    my @cmdline_args = @_;

    die Cpanel::Exception::create('RootRequired')->to_string_no_id() unless ( $> == 0 && $< == 0 );
    return Pod::Usage::pod2usage( -exitval => 'NOEXIT', -output => \*STDERR, -verbose => 99, -sections => [qw(NAME DESCRIPTION SYNOPSIS)] ) if !@cmdline_args;

    my $opts = _parse_and_validate_opts( \@cmdline_args );

    # -1 to get the right exit code
    return Pod::Usage::pod2usage( -exitval => 'NOEXIT', -output => \*STDOUT, -verbose => 99, -sections => [qw(NAME DESCRIPTION SYNOPSIS)] ) - 1 if $opts->{'help'};
    return Pod::Usage::pod2usage( -exitval => 'NOEXIT', -verbose => 2 ) - 1 if $opts->{'man'};

    if ( my @enabled_operations = map { __PACKAGE__->can( $_ =~ s/\-/_/gr ) } sort grep { $opts->{'operations'}->{$_} } keys %{ $opts->{'operations'} } ) {
        process_users( $opts->{'resellers'}, \@enabled_operations )
          if $opts->{'resellers'} && scalar @{ $opts->{'resellers'} };

        process_acl_lists( $opts->{'acl-lists'}, \@enabled_operations )
          if $opts->{'acl-lists'} && scalar @{ $opts->{'acl-lists'} };

        return 0;
    }

    die Cpanel::Exception->create_raw("[!] You must specify a operation to perform. See --help\n")->to_string_no_id();
}

sub process_users {
    my $resellers_to_process_ar = shift;
    my $callbacks_ar            = shift;

    Cpanel::LoadModule::load_perl_module('Cpanel::Reseller');
    Cpanel::LoadModule::load_perl_module('Whostmgr::Resellers');

    # TODO: The current interfaces to the RESELLERS_FILE do not provide
    # any way to do a 'mass-edit'. Depending on how slow this process is,
    # we might need to implement one.
    my %current_reseller_acls = Cpanel::Reseller::getresellersaclhash();
    foreach my $reseller ( @{$resellers_to_process_ar} ) {

        # We validated resellers beforehand, but just in case something
        # changed between that check, and the getresellersaclhash call, check again.
        next unless exists $current_reseller_acls{$reseller};
        _output("[*] Processing reseller: '$reseller'...\n");
        foreach my $cr ( @{$callbacks_ar} ) {
            $cr->(
                {
                    name         => $reseller,
                    current_acls => $current_reseller_acls{$reseller},
                }
            );
        }

        # set_reseller_acls requires the ACLs to have a 'acl-' prefix
        Whostmgr::Resellers::set_reseller_acls( $reseller, { map { 'acl-' . $_ => 1 } keys %{ $current_reseller_acls{$reseller} } } );
        _output("[+] Processed reseller: '$reseller'\n");
    }

    return 0;
}

sub process_acl_lists {
    my $acl_lists_to_process_ar = shift;
    my $callbacks_ar            = shift;

    Cpanel::LoadModule::load_perl_module('Whostmgr::ACLS');

    foreach my $acl_list ( @{$acl_lists_to_process_ar} ) {
        my $list_file = "$Cpanel::ConfigFiles::ACL_LISTS_DIR/$acl_list";
        next unless -f $list_file;

        _output("[*] Processing ACL list: '$acl_list'...\n");
        if ( open( my $acl_fh, '<', $list_file ) ) {
            my $acls = { map { split /=/, $_, 2 } grep { !/^\s*$/ } map { s/\n//r } readline($acl_fh) };
            close($acl_fh);

            foreach my $cr ( @{$callbacks_ar} ) {
                $cr->(
                    {
                        name         => $acl_list,
                        current_acls => $acls,
                    }
                );
            }

            Whostmgr::ACLS::save_acl_list(
                'acllist' => $acl_list,
                ( map { 'acl-' . $_ => 1 } grep { $acls->{$_} } keys %{$acls} )
            );

            _output("[+] Processed ACL list: '$acl_list'\n");
        }
        else {
            _output("[!] Failed to process ACL list '$acl_list': $!\n");
        }
    }

    return 0;
}

my $defaults_to_apply_hr;

sub add_default_privs {
    my $to_process_hr = shift;

    $defaults_to_apply_hr //= { map { $_ => 1 } @{ Whostmgr::ACLS::Data::get_default_acls() } };
    _output("\t[*] Adding default privileges to '$to_process_hr->{'name'}'...\n");
    %{ $to_process_hr->{'current_acls'} } = (
        %{ $to_process_hr->{'current_acls'} },
        %{$defaults_to_apply_hr}
    );
    _output("\t[+] Added default privileges to '$to_process_hr->{'name'}'.\n");
    return 1;
}

sub fix_disallow_shell {
    my $to_process_hr = shift;

    _output("\t[*] Fixing 'disallow-shell' privilege for '$to_process_hr->{'name'}'...\n");
    my $had_disallow_shell = delete $to_process_hr->{'current_acls'}->{'disallow-shell'};
    if ( !$had_disallow_shell ) {
        %{ $to_process_hr->{'current_acls'} } = (
            %{ $to_process_hr->{'current_acls'} },
            'allow-shell' => 1,
        );
    }
    _output("\t[+] Fixed 'disallow-shell' privilege for '$to_process_hr->{'name'}'.\n");
    return 1;
}

# for mocking
sub _output { return print @_; }

sub _parse_and_validate_opts {
    my $cmdline_args_ar = shift;

    my $opts = {
        'help'       => 0,
        'man'        => 0,
        'operations' => {
            'add-default-privs'  => 0,
            'fix-disallow-shell' => 0,
        },
        'all-resellers'       => 0,
        'specified_resellers' => {},
        'all-acl-lists'       => 0,
        'specified_acl_lists' => {}
    };
    Getopt::Long::GetOptionsFromArray(
        $cmdline_args_ar,
        'help|h' => \$opts->{'help'},
        'man'    => \$opts->{'man'},

        # What should be changed?
        'reseller=s' => sub { $opts->{'specified_resellers'}->{ $_[1] }++; },    # Avoid dupes
        'acl-list=s' => sub { $opts->{'specified_acl_lists'}->{ $_[1] }++; },    # Avoid dupes
        'all-resellers' => \$opts->{'all-resellers'},
        'all-acl-lists' => \$opts->{'all-acl-lists'},

        # What changes should be made?
        'add-default-privs'  => \$opts->{'operations'}->{'add-default-privs'},
        'fix-disallow-shell' => \$opts->{'operations'}->{'fix-disallow-shell'},
    ) or die Cpanel::Exception->create_raw("[!] Invalid usage. See --help\n")->to_string_no_id();
    return $opts if $opts->{'help'} || $opts->{'man'};

    $opts->{'resellers'} = _validate_resellers($opts);
    $opts->{'acl-lists'} = _validate_acl_lists($opts);

    die Cpanel::Exception->create_raw("[!] You must specify a mode of operation. See --help\n")->to_string_no_id()
      unless $opts->{'resellers'} // $opts->{'acl-lists'};

    return $opts;
}

sub _validate_resellers {
    my $opts = shift;

    if ( $opts->{'all-resellers'} ) {
        Cpanel::LoadModule::load_perl_module("Whostmgr::Resellers::List");
        Cpanel::LoadModule::load_perl_module('Cpanel::Config::HasCpUserFile');
        return [

            # Skip 'resellers without a domain' when processing all resellers on the system:
            # https://documentation.cpanel.net/display/CKB/How+to+Create+a+WHM+Reseller+Without+An+Associated+Domain
            #
            # These resellers are created "out of band" by editing the resellers file,
            # so altering them should be left up to the server administrators.
            grep { Cpanel::Config::HasCpUserFile::has_cpuser_file($_) }
              keys %{ Whostmgr::Resellers::List::list() }
        ];
    }
    elsif ( my @specified_resellers = keys %{ $opts->{'specified_resellers'} } ) {
        Cpanel::LoadModule::load_perl_module("Whostmgr::Resellers::Check");
        if ( my @invalid_resellers = grep { !Whostmgr::Resellers::Check::is_reseller($_) } @specified_resellers ) {
            die Cpanel::Exception->create_raw( "[!] Invalid resellers specified:\n" . join( "\n", map { " " x 8 . $_ } @invalid_resellers ) . "\n" )->to_string_no_id();
        }

        return \@specified_resellers;
    }

    return;
}

sub _validate_acl_lists {
    my $opts = shift;

    if ( $opts->{'all-acl-lists'} ) {
        if ( opendir my $dh, $Cpanel::ConfigFiles::ACL_LISTS_DIR ) {
            return [ grep { $_ !~ m/^\.+$/ && -f "$Cpanel::ConfigFiles::ACL_LISTS_DIR/$_" } readdir($dh) ];
        }
    }
    elsif ( my @specified_acl_lists = keys %{ $opts->{'specified_acl_lists'} } ) {
        if ( my @invalid_acl_lists = grep { !-f "$Cpanel::ConfigFiles::ACL_LISTS_DIR/$_" } @specified_acl_lists ) {
            die Cpanel::Exception->create_raw( "[!] Invalid acl-lists specified:\n" . join( "\n", map { " " x 8 . $_ } @invalid_acl_lists ) . "\n" )->to_string_no_id();
        }
        return \@specified_acl_lists;
    }

    return;
}

1;

__END__

=encoding utf8

=head1 NAME

fix_reseller_acls

=head1 DESCRIPTION

Utility to update reseller privileges and ACL lists.

=head1 SYNOPSIS

    fix_reseller_acls [OPERATION] [MODE]

    Operations:
    --add-default-privs            Add the default set of privileges.
    --fix-disallow-shell           Clean up the 'disallow-shell' privilege.

    Modes:
   --reseller [reseller]           Update the specified reseller.
   --all-resellers                 Update all resellers on the system.

   --acl-list [acl-list]           Update the specified ACL list.
   --all-acl-lists                 Update all ACL lists on the system.

   --help                          This documentation.
   --man                           Full documentation.

=head1 Operations

Specify at least one operation.

=over

=item B<--add-default-privs>

Add the default set of privileges, introduced in v68, to the set of resellers
and ACLS lists specified.

    acct-summary
    basic-system-info
    basic-whm-functions
    cors-proxy-get
    cpanel-integration
    cpanel-api
    create-user-session
    digest-auth
    generate-email-config
    list-pkgs
    manage-api-tokens
    manage-dns-records
    manage-oidc
    manage-styles
    mysql-info
    ns-config
    ssl-info
    track-email

=item B<--fix-disallow-shell>

Remove the 'disallow-shell' privilege from the set of resellers and specified ACL lists.

If the C<disallow-shell> privilege is set, then the script will remove it.
If the C<disallow-shell> privilege is not set, then the script adds the C<allow-shell> privilege.

=back

=head1 Modes

Specify at least one mode.

=over

=item B<--all-resellers>

Process all of the resellers on the system. This option overrides B<--reseller>.

B<Note>: The script does not process resellers without an associated domain in this mode.

=item B<--reseller [reseller-username]>

Process the reseller specified.

Specify this option multiple times to process mutiple resellers.

=item B<--all-acl-lists>

Process all ACL lists on the system. This option overrides B<--acl-list>.

=item B<--acl-list [acl-list]>

Process the ACL list specified.

Specify this option multiple times to process mutiple ACL lists.

=back

=head1 EXAMPLES

=over

=item C<--add-default-privs --fix-disallow-shell --all-resellers>

Update the privileges for all resellers on the system to include the default privilege and clean
up the C<disallow-shell> privilege.

=item C<--add-default-privs --reseller myreseller>

Update the privileges for the I<myreseller> reseller to include the new default privileges.

=item C<--add-default-privs --fix-disallow-shell --all-acl-lists>

Update all of the ACL lists on the system to include the default privileges, and clean up the
C<disallow-shell> privilege.

=back

=cut


@KyuuKazami