#!/usr/local/cpanel/3rdparty/bin/perl
# cpanel - scripts/suspendacct Copyright 2019 cPanel, L.L.C.
# All rights Reserved.
# copyright@cpanel.net http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
package scripts::suspendacct;
use strict;
## no critic qw(TestingAndDebugging::RequireUseWarnings) -- suspendacct is not yet warnings safe
use Try::Tiny;
use Whostmgr::ACLS ();
use Cpanel::Auth::Digest::DB::Manage ();
use Cpanel::HttpUtils::ApRestart::BgSafe ();
use Cpanel::Auth::Shadow ();
use Cpanel::AcctUtils::AccountingLog ();
use Cpanel::AcctUtils::DomainOwner::Tiny ();
use Cpanel::AcctUtils::Owner ();
use Cpanel::AcctUtils::Domain ();
use Cpanel::PwCache::Clear ();
use Cpanel::Validate::Domain::Tiny ();
use Cpanel::Encoder::Tiny ();
use Cpanel::Exception ();
use Cpanel::Dovecot::Action ();
use Cpanel::AccessIds::ReducedPrivileges ();
use Cpanel::AcctUtils::Suspended ();
use Cpanel::SafetyBits ();
use Cpanel::Config::CpUserGuard ();
use Cpanel::ConfigFiles ();
use Cpanel::FileUtils::Copy ();
use Cpanel::FileUtils::Match ();
use Cpanel::FileUtils::Write ();
use Cpanel::Hooks ();
use Cpanel::Hostname ();
use Cpanel::IP::Remote ();
use Cpanel::MysqlUtils::Suspension ();
use Cpanel::SafeFile ();
use Cpanel::Sys::Kill ();
use Cpanel::Passwd::Shell ();
use Cpanel::ServerTasks ();
use Cpanel::Session::SinglePurge ();
use Cpanel::PwCache ();
use Cpanel::Validate::Username ();
use Cpanel::Validate::Domain::Normalize ();
use Cpanel::Quota::Temp ();
use Whostmgr::Accounts::Suspend ();
use Whostmgr::Accounts::Email ();
use Whostmgr::Accounts::SuspensionData::Writer ();
use AcctLock ();
use Cpanel::Notify ();
use Getopt::Long ();
exit( run(@ARGV) ) unless caller;
sub run { ## no critic qw(Subroutines::ProhibitExcessComplexity)
my (@args) = @_;
my $usage = 0;
my $force = 0;
usage() if !@args;
Getopt::Long::GetOptionsFromArray(
\@args,
'help|usage' => \$usage,
'force' => \$force,
);
my $user = $args[0];
$user =~ s/\///g;
my $reason = Cpanel::Encoder::Tiny::safe_html_decode_str( $args[1] );
$reason =~ s/[=\n\0]//g;
my $prevent_reseller_unsuspend = $args[2];
usage() if $usage;
usage() if ( !$user || is_forbidden_user($user) );
local $ENV{'USER'} = $ENV{'USER'};
local $ENV{'REMOTE_USER'} = $ENV{'REMOTE_USER'};
if ( ( !$ENV{'USER'} || !$ENV{'REMOTE_USER'} ) && $> == 0 ) {
$ENV{'REMOTE_USER'} = 'root'; ## no critic qw(Variables::RequireLocalizedPunctuationVars) - local above
$ENV{'USER'} = 'root'; ## no critic qw(Variables::RequireLocalizedPunctuationVars) - local above
}
# Needed for changing quotas.
Whostmgr::ACLS::init_acls();
my $pass = ( Cpanel::PwCache::getpwnam($user) )[1];
if ( !defined $pass ) {
$user = Cpanel::AcctUtils::DomainOwner::Tiny::getdomainowner( $user, { 'default' => '' } );
if ( !$user ) {
die "Invalid user\n";
}
$pass = ( Cpanel::PwCache::getpwnam($user) )[1];
if ( !defined $pass ) {
die "Invalid user\n";
}
}
if ( !$user || is_forbidden_user($user) ) { die "Invalid user $user"; }
if ( $prevent_reseller_unsuspend && !Whostmgr::ACLS::hasroot() ) {
print "Can not prevent resellers from unsuspending this account without the 'all' ACL.\n";
return 1;
}
if ( Cpanel::AcctUtils::Suspended::is_suspended($user) && !$force ) {
print "User '$user' is already suspended. Provide '--force' argument if you wish to do this.\n";
return 1;
}
# this one doesn't seem to exist
if ( !do_hook( $user, $reason, $prevent_reseller_unsuspend, 'pre' ) ) {
print "Pre-suspend hook script returned failure.\n";
return 1;
}
system '/usr/local/cpanel/scripts/presuspendacct', @args if -x '/usr/local/cpanel/scripts/presuspendacct';
$ENV{'REMOTE_USER'} ||= 'root';
Whostmgr::ACLS::init_acls();
my ( $homedir, $shell ) = ( Cpanel::PwCache::getpwnam($user) )[ 7, 8 ];
my $host;
AcctLock::acctlock();
print "Changing Shell to /bin/false...";
try {
Cpanel::Passwd::Shell::update_shell_without_acctlock( 'user' => $user, 'shell' => '/bin/false' );
}
catch {
print Cpanel::Exception::get_string($_);
};
print "Done\n";
print "Locking Password...";
my ( $status, $statusmsg ) = Cpanel::Auth::Shadow::update_shadow_without_acctlock( $user, '!!' . ( Cpanel::PwCache::getpwnam($user) )[1] );
print $statusmsg if !$status;
print "Done\n";
AcctLock::acctunlock();
Cpanel::Auth::Digest::DB::Manage::lock($user) if Cpanel::Auth::Digest::DB::Manage::has_entry($user);
my $owner = Cpanel::AcctUtils::Owner::getowner($user);
$owner =~ s/\n//g;
my $domain = Cpanel::AcctUtils::Domain::getdomain($user);
if ( $owner eq '' || $owner eq 'root' || $user eq $owner ) {
$host = Cpanel::Hostname::gethostname();
}
else {
$host = $domain;
}
if ( !$host ) {
$host = Cpanel::Hostname::gethostname();
}
my $susp_info_hr = { shell => $shell };
my $suspend_data = Whostmgr::Accounts::SuspensionData::Writer->new();
if ( $prevent_reseller_unsuspend == 1 ) {
$suspend_data->suspend_locked( $user, $reason // q<>, $susp_info_hr );
}
else {
$suspend_data->suspend_unlocked( $user, $reason // q<>, $susp_info_hr );
}
# session deletion and then
# kill -9 must be done after the suspendfile is created
# to ensure cpsrvd will not allow any more processes
# to be created as the user. this also avoids having to check
# each cpsrvd request
Cpanel::Session::SinglePurge::purge_user( $user, 'suspend' );
Cpanel::Sys::Kill::kill_pids_owned_by( $user, '-9' );
my $cpuser_guard = Cpanel::Config::CpUserGuard->new($user);
my $cpuser_data = $cpuser_guard->{'data'};
my @DNS = ( $cpuser_data->{'DOMAIN'} );
if ( exists $cpuser_data->{'DOMAINS'} ) {
push @DNS, @{ $cpuser_data->{'DOMAINS'} };
}
mkdir( "$Cpanel::ConfigFiles::MAILMAN_ROOT/suspended.lists", 0755 );
{
my $tempquota = Cpanel::Quota::Temp->new( user => $user );
$tempquota->disable();
if ( -f "$homedir/etc/webdav/shadow" && !-l "$homedir/etc/webdav/shadow" ) {
print "Suspending webdav users\n";
suspendshadowfile( $user, "$homedir/etc/webdav/shadow" );
}
foreach my $dns (@DNS) {
$dns = Cpanel::Validate::Domain::Normalize::normalize( $dns, 1 );
next if !Cpanel::Validate::Domain::Tiny::validdomainname($dns);
if ( -f "${homedir}/etc/${dns}/shadow"
&& !-l "${homedir}/etc/${dns}/shadow" ) {
print "Suspending email account logins for ${dns} .... ";
suspendshadowfile( $user, "${homedir}/etc/${dns}/shadow" );
print "Done\n";
}
}
#This will recreate the files in the user homedir,
#so we want to do it under a quota-lift.
Cpanel::Dovecot::Action::flush_all_auth_caches_for_user($user);
}
my $dns_list = join( '|', map { quotemeta($_) } @DNS );
my $list_files = Cpanel::FileUtils::Match::get_matching_files( "$Cpanel::ConfigFiles::MAILMAN_ROOT/lists", "_(?:$dns_list)" . '$' );
foreach my $list ( @{$list_files} ) {
my $suspended_list = $list;
$suspended_list =~ s/\/lists\//\/suspended.lists\//;
if ( -e $suspended_list ) { rename( $suspended_list, $suspended_list . '.' . time() ) }
rename( $list, $suspended_list );
}
print "Suspending mysql users\n";
Cpanel::MysqlUtils::Suspension::suspend_mysql_users($user);
# FIXME: Everything in this script should eventually be done via
# this function call (or similar logic).
try {
Whostmgr::Accounts::Suspend->new(
$user,
reason => $reason,
prevent_reseller_unsuspend => $prevent_reseller_unsuspend,
);
}
catch {
warn Cpanel::Exception::get_string($_);
};
my %account_creation_notification = (
'user' => $user,
'user_domain' => $domain,
'reason' => $reason,
'env_remote_user' => $ENV{'REMOTE_USER'},
'env_user' => $ENV{'USER'},
'host_server' => $host,
'origin' => 'Suspend Account',
'source_ip_address' => Cpanel::IP::Remote::get_current_remote_ip(),
);
# send root notification
Cpanel::Notify::notification_class(
'class' => 'suspendacct::Notify',
'application' => 'suspendacct::Notify',
'constructor_args' => [%account_creation_notification]
);
# send one to account reseller as well as long as they are not root
if ( $owner ne 'root' ) {
Cpanel::Notify::notification_class(
'class' => 'suspendacct::Notify',
'application' => 'suspendacct::Notify',
'constructor_args' => [ %account_creation_notification, 'to' => $owner, 'username' => $owner ]
);
}
if ( $pass =~ /^\!/ || $pass =~ /^\*/ ) {
print "Account previously suspended (password was locked).\n";
}
$cpuser_data->{'SUSPENDTIME'} = time();
$cpuser_data->{'SUSPENDED'} = 1;
$cpuser_guard->save();
if ( !-e "/var/spool/cron.suspended" ) {
mkdir( "/var/spool/cron.suspended", 0700 );
}
if ( -f "/var/spool/cron/${user}" ) {
link( "/var/spool/cron/${user}", "/var/spool/cron.suspended/${user}" );
unlink("/var/spool/cron/${user}");
}
Cpanel::SafetyBits::safe_chmod( 0000, $user, "${homedir}/public_ftp" );
my $tempquota = Cpanel::Quota::Temp->new( user => $user );
$tempquota->disable();
print "Suspending websites...\n";
_generate_account_suspension_include($user);
$tempquota->restore();
_suspend_ftp($user);
Cpanel::ServerTasks::schedule_task( ['CpDBTasks'], 10, "ftpupdate" );
print "Suspending outgoing email....";
Whostmgr::Accounts::Email::suspend_outgoing_email( 'user' => $user );
print "Done\n";
print ${user} . "'s account has been suspended\n";
Cpanel::AcctUtils::AccountingLog::append_entry( 'SUSPEND', [ $user, $domain, $reason ] );
Cpanel::PwCache::Clear::clear_global_cache();
do_hook( $user, $reason, $prevent_reseller_unsuspend, 'post' );
system '/usr/local/cpanel/scripts/postsuspendacct', @args if -x '/usr/local/cpanel/scripts/postsuspendacct';
return;
}
# TODO: Refactor this function (along with the unsuspension logic)
# to a Whostmgr::Accounts::Suspension::* module.
sub _suspend_ftp {
my ($username) = @_;
my $ftpfile = "$Cpanel::ConfigFiles::FTP_PASSWD_DIR/$username";
my $ftplock = Cpanel::SafeFile::safelock($ftpfile); # Manipulation of these files isn't thread safe.
if ( -e $ftpfile && !-e $ftpfile . '.suspended' && -e "/var/cpanel/suspended/$username" ) {
print "Suspending FTP accounts...\n";
my ( $ok, $err ) = Cpanel::FileUtils::Copy::copy( $ftpfile, qq{$ftpfile.suspended} );
if ($err) {
warn "Could not copy $ftpfile to $ftpfile.suspended: $err";
}
else {
Cpanel::FileUtils::Write::overwrite_no_exceptions( $ftpfile, '# Account suspended', 0640 );
}
}
Cpanel::SafeFile::safeunlock($ftplock);
return;
}
sub is_forbidden_user {
my ($user) = @_;
return ( grep { $user eq $_ } Cpanel::Validate::Username::list_reserved_usernames() ) ? 1 : 0;
}
# helpers
sub suspendshadowfile {
my ( $user, $file ) = @_;
# user cannot be root, tested previously
# we should not write file as root in the user's home directory...
my $access_ids = Cpanel::AccessIds::ReducedPrivileges->new($user);
return _suspendshadowfile($file);
}
sub _suspendshadowfile {
my ($file) = @_;
my @shadow_file;
my $shadowlock = Cpanel::SafeFile::safeopen( \*SHF, '<', $file );
if ($shadowlock) {
@shadow_file = <SHF>;
Cpanel::SafeFile::safeclose( \*SHF, $shadowlock );
undef $shadowlock;
}
else {
if ( -e $file ) {
warn "Failed to read $file: $!";
return;
}
else {
return 1;
}
}
if (@shadow_file) {
$shadowlock = Cpanel::SafeFile::safeopen( \*SHF, '>', $file );
if ($shadowlock) {
foreach (@shadow_file) {
chomp;
my @DC = split( /:/, $_ );
foreach my $field ( 1, 8 ) {
$DC[$field] = '' unless defined $DC[$field];
if ( $DC[$field] !~ m/^\*LOCKED\*/ ) {
$DC[$field] = "*LOCKED*" . $DC[$field];
}
}
print SHF join( ':', map { defined $_ ? $_ : '' } @DC ) . "\n";
}
truncate( SHF, tell(SHF) );
Cpanel::SafeFile::safeclose( \*SHF, $shadowlock );
}
else {
warn "Failed to update $file: $!";
return;
}
return 1;
}
else {
return 1;
}
}
sub do_hook {
my ( $user, $reason, $prevent_reseller_unsuspend, $stage ) = @_;
my ( $result, $hooks_msgs ) = Cpanel::Hooks::hook(
{
'category' => 'Whostmgr',
'event' => 'Accounts::suspendacct',
'stage' => $stage,
'escalateprivs' => 1,
},
{
'args' => {
'user' => $user,
'reason' => $reason,
'disallowun' => $prevent_reseller_unsuspend,
},
'result' => 1,
'user' => 'root',
},
);
if ( ref $hooks_msgs eq 'ARRAY' && @$hooks_msgs != 0 ) {
foreach my $error ( @{$hooks_msgs} ) {
print $error;
}
return 0;
}
return 1;
}
sub _generate_account_suspension_include {
my ($user) = @_;
require "/usr/local/cpanel/scripts/generate_account_suspension_include"; ## no critic qw(Modules::RequireBarewordIncludes) -- refactoring this is too large
generate_account_suspension_include::update_include( 0, $user );
Cpanel::HttpUtils::ApRestart::BgSafe::restart();
return 1;
}
sub usage {
my $p = $0;
$p =~ s@^.+/(.+)$@$1@;
print <<EOF;
Usage: $p user [reason] [disallow] [--usage | --help | --force]
Suspend a user's account with possibly a more stringent
suspension as determined by disallow.
where
user -- is a valid user name (required)
reason -- is a quote bound description for the suspension
and is written into the /var/cpanel/suspended/<user> file
(optional)
disallow -- is for additionally generating a user.lock file
in /var/cpanel/suspended and to issue this the argument needs to
be 1 (optional)
Note that the order of user, reason and disallow must be maintained.
Users with reserved usernames (e.g. root and nobody) cannot be suspended.
The --force argument will allow for the suspension process to be run against an
already suspended account.
Now supports driving instructions via --help, --usage
EOF
exit; ## no critic(Cpanel::NoExitsFromSubroutines)
}
1;
Copyright 2K16 - 2K18 Indonesian Hacker Rulez