#!/usr/bin/perl

#######  meta-verify-raq3.pl
#
#  system & database verification and integrity check utility
#
#  Duncan Laurie (duncan@cobalt.com)
#  (c) 2000 Cobalt Networks, Inc
#
#
#######  OPERATION
#
#  meta-verify-raq3.pl [-a|i] [-v] [-h]
#
#  -i   verify & repair invalid users [default]
#  -a   verify & repair valid users
#  -v   increase verbose warning level
#  -h   usage help
#
#
#######  USES
#
#  - Verify and repair virtual site user accounts.
#  - Maintain intergrity of the RaQ3 system configuration files.
#  - Ensure coherency between System and Meta/postgreSQL backend.
#
#
#######  DESCRIPTION
#
#  The RaQ3 (all varieties pre Update 2.0) contains a bug w.r.t. handling
#  single quote characters in vacation messages and usernames that
#  begin with a decimal digit.  The result of this bug is that some or even
#  all of your users could become "detached" from the Web GUI -- they will
#  exist on the system but not show up in the Admin interface.
#
#  This script attempts to detect and repair coherency problems that may
#  result from this bug.  It will find invalid (detached) users and
#  present them as Meta object which can be saved to the postgreSQL backend.
#
#
#######  REQUIREMENTS
#
#  RaQ3 / RaQ3i / RaQ3-ja
#
#
#######  CHANGELOG
#
#  version 1.0 (Feb 03 2000)
#  - initial release
#  - fix incorrect reporting of frontpage state
#
#  version .9  (Feb 02 2000)
#  - verify valid user accounts, prompt database update when changes detected
#  - prompts for and allows database deletion of sites that do not exist in
#    the system configuration
#  - fix int() conversion & mismatched parens
#
#  version .8  (Feb 01 2000)
#  - @{ union,intersect,symmetric difference } for database/system user lists
#  - detect invalid system users correctly
#
#  version .7  (Feb 01 2000)
#  - meta object containing all user fields, completed with information from
#    current system configuration and state information
#
#  version .1  (Jan 31 2000)
#  - initial creation
#
#
#######  
require Cobalt::Meta;
require Cobalt::User;
require Cobalt::Vacation;
require Cobalt::Email;
require Cobalt::List;
require Cobalt::Fpx;
require Cobalt::Quota;

use Getopt::Std;

# program info
use vars qw($Title $Product $Version $Author $Company);
$Product = "meta-verify-raq3.pl";
$Title   = '"system & database verifier"';
$Author  = "Duncan Laurie (duncan\@cobalt.com)";
$Company = "(c) 2000 Cobalt Networks, Inc";
$Version = "1.0-RaQ3";

# program header
printf("\n");
printf("%s\n", $Product);
printf("%s\n", $Title);
printf("version %s\n", $Version);
printf("%s\n", $Author);
printf("%s\n", $Company);

# global variables
use vars qw($e %userCOUNT @userALL @userGOOD @userBAD);
use vars qw(@userFIELDS @userARRAYS);
use vars qw(@all_DB @all_SYS $userX $User);
use vars qw($opt_h $opt_v $opt_a $opt_i $Verbose);
use vars qw($login $message $field $DB_field $SYS_field);

# format for output of user messages
format PRINT_MSG =
                | @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
                  $message
.

# format for output of user messages
format PRINT_USER =
@>>>>>>>>>>>>>> | @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$login,           $message
.

# format for output when handling user errors
format PRINT_USER_ERROR =
@>>>>>>>>>>>>>> | ERROR @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$login,                 $message
.

# format for output when processing users
format PRINT_FIELD =
@>>>>>>>>>>>>>> | @>>>>>>>>>>>>> = @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$login,           $field,          $message
.

# format for error output when comparing meta+system fields
format PRINT_FIELD_ERROR =
@>>>>>>>>>>>>>> | ERROR @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$login,                 $field
                | SYSTEM @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
                         $SYS_field
                | DATABASE @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
                           $DB_field
.

use strict;

# some global variables
# user fields to compare when verifying all users
@userFIELDS = ("name", "fullname", "uid", "vsite", "quota",
	       "vacation", "admin", "shell", "apop", "fpx", "suspend");
# these fields are to be treated as arrays
# where order does NOT matter
@userARRAYS = ("forward", "aliases");

# the global database views and Meta objects
$userX   = (Cobalt::Meta->new("type" => "users"));
# all virtual site users in the database
@all_DB  = ($userX->getall());
# all virtual site users in the password file
@all_SYS = (Cobalt::User::user_list());
# remove admin from both sets
@all_SYS = grep !/\badmin\b/, @all_SYS;
@all_DB  = grep !/\badmin\b/, @all_DB;

# command-line arguments, verbose level
getopts("vaih");

# usage help
if ($opt_h)
{
    printf("\n");
    printf(" usage: %s [-a|i] [-v] [-h]\n", $Product);
    printf("\n");
    printf("   -i   verify & repair invalid users [default]\n");
    printf("   -a   verify & repair valid users\n");
    printf("   -v   increase warning level\n");
    printf("   -h   usage help\n");
    printf("\n");
    exit 1;
}
$Verbose = ($opt_v) ? 1 : 0;

# find coherency problems between database and system
printf("\nchecking system+databse coherency");
printf("\n\n") if ($Verbose);

foreach $e (@all_DB, @all_SYS)
{
    # count users
    next if ($e =~ /\badmin\b/);
    $userCOUNT{$e}++;
}

foreach $e (keys %userCOUNT)
{
    # union -
    # all users in database or system
    push(@userALL, $e);

    if ($userCOUNT{$e} == 2)
    {
	# intersection -
	# users both in database and in system
	push(@userGOOD, $e);
    }
    else
    {
	# symmetric difference -
	# users either not in database or not in system
	push(@userBAD, $e);
    }
}

# output USER NUMBERS
if ($Verbose)
{
    printf("       database | %d\n", scalar(@all_DB));
    printf("         system | %d\n", scalar(@all_SYS));
    printf("          total | %d\n", scalar(@userALL));
    printf("          valid | %d\n", scalar(@userGOOD));
    printf("        invalid | %d\n", scalar(@userBAD));
}

# if no invalid and -a not specified
if (!scalar(@userBAD) && !$opt_a)
{
    printf("\ndone\n");
    exit(0);
}

# if -a specified
if ($opt_a)
{
    printf("\nverifying all valid users in system+database\n\n");
    &examine_users(@userGOOD);
    printf("\ndone\n");
}

# if some invalid and -a not specified
if (!$opt_a || $opt_i)
{
    printf("\nverifying invalid system users\n");
    &examine_invalid_users(@userBAD);
    printf("\ndone\n");
}

exit 0;

1;

sub print_msg
{
    $message = "@_";
    $~ = 'PRINT_MSG';
    write();
}

sub print_user
{
    $message = "@_";
    $login = "$User";
    $~ = 'PRINT_USER';
    write();
}

sub print_user_error
{
    $message = "(@_)";
    $login = "$User";
    $~ = 'PRINT_USER_ERROR';
    write();
}

sub print_field
{
    ($field,$message) = (@_);
    $login = "$User";
    $~ = 'PRINT_FIELD';
    write();
}

sub print_field_error
{
    ($field,$DB_field,$SYS_field) = (@_);
    $field = "($field)";
    $DB_field = "($DB_field)";
    $SYS_field = "($SYS_field)";
    $login = "$User";
    $~ = 'PRINT_FIELD_ERROR';
    write();
}

# examine_invalid_users ()
#
#   check the userLIST for invalid users and
#   correct the problem if possible
#
#   arg:  user list to check
#
sub examine_invalid_users ()
{
    my (@userLIST) = (@_);
    
    foreach $User (@userLIST)
    {
	# always skip admin
	next if ($User =~ /\badmin\b/);

	my $inDB  = 0;
	my $inSYS = 0;

	printf("\n");

	# see if this user exists in the database
	if (grep(/\b$User\b/, @all_DB))
	{
	    $inDB = 1;
	}
	else
	{
	    $inDB = 0;
	    &print_user_error("user does not exist in database");
	}

	# see if this user exists in the system
	&print_msg("retrieving user information from system...") if ($Verbose);
	$userX = &build_user_system($User);
	if ($userX)
	{
	    $inSYS = 1;
	}
	else
	{
	    &print_user_error("does not exist in system");
	    $inSYS = 0;
	}

	# in both system and database
	# this should never happen
	if ($inSYS && $inDB)
	{
	    &print_user_error("user is already valid");
	    $userX->DESTROY();
	    next;
	}

	# in system but not database
	if ($inSYS)
	{
	    my $input;

	    # confirm user add
	    while ($input !~ /^[YyNn]/)
	    {
		printf("                | save database entry? [y/n/p] ");
		$input = <STDIN>;

		# print user info
		if ($input =~ /^p/)
		{
		    $Verbose++;
		    $userX->DESTROY();
		    $userX = &build_user_system("$User");
		    $Verbose--;
		}
	    }

	    # always give the chance to bail
	    next if ($input =~ /^[Nn]/);

	    # save user
	    unless ($userX->save())
	    {
		&print_user_error("unable to save user");
		$userX->DESTROY();
		next;
	    }

	    &print_user("saved");
	    $userX->DESTROY();
	    next;
	}

	# in database but not system
	if ($inDB)
	{
	    my $input = '';
	    my $userY = Cobalt::Meta->new("type" => "users");

	    # confirm database entry delete
	    while ($input !~ /^[YyNn]/)
	    {
		printf("                | remove database entry? [y/n] ");
		$input = <STDIN>;
	    }

	    # always give the chance to bail
	    next if ($input !~ /^[Yy]/);

	    # delete user entry
	    $userY->retrieve("$User");
	    unless ($userY->delete())
	    {
		&print_user_error("unable to remove user");
		$userY->DESTROY();
		next;
	    }

	    $userY->DESTROY();
	    &print_user("removed");
	    next;
	}

	# not in database or system
	&print_user_error("user not in database OR system");
	next;
    }

    return;
}

# examine_users ()
#
#   check the LIST for valid users and verify
#   coherency of fields between system and database
#
#   correct the problem if possible
#
#   arg:  user list to check
#
sub examine_users ()
{
    my (@userLIST) = (@_);

    foreach $User (@userLIST)
    {
	# always skip admin
	next if ($User =~ /\badmin\b/);

	# this user entry from the database
	my $userDB = (Cobalt::Meta->new("type" => "users"));
	unless ($userDB->retrieve("$User"))
	{
	    &print_user_error("user non-existant in database");
	    next;
	}

	# get user info from the system
	my $userSYS = &build_user_system("$User");
	unless ($userSYS)
	{
	    &print_user_error("user does not exist in system");
	    next;
	}

	# compare fields
	my $errcount = 0;
	foreach my $fld (@userFIELDS)
	{
	    if ($userDB->{$fld} ne $userSYS->{$fld})
	    {
		$errcount++;
		&print_field_error($fld, $userDB->{$fld}, $userSYS->{$fld});
	    }
	}

	foreach my $fld (@userARRAYS)
	{
	    my ($e,@aDB,@aSYS,%count,@diff);

	    # turn flat string into list
	    @aDB = split(' ', $userDB->{$fld});
	    @aSYS = split(' ', $userSYS->{$fld});

	    # compute difference of lists
	    foreach $e (@aDB, @aSYS) { $count{$e}++ }
	    foreach $e (keys %count) { push (@diff, $e) unless ($count{$e} == 2) }

	    # if difference then error
	    if (scalar(@diff))
	    {
		$errcount++;
		&print_field_error($fld, $userDB->{$fld}, $userSYS->{$fld});
	    }
	}

	if ($errcount)
	{
	    my $input;

	    # confirm database update
	    while ($input !~ /^[YyNn]/)
	    {
		printf("                | update database entry? [y/n] ");
		$input = <STDIN>;
	    }

	    # always give the chance to bail
	    if ($input !~ /^[Yy]/)
	    {
		printf("\n");
		next;
	    }

	    # save user
	    unless ($userSYS->save())
	    {
		&print_user_error("unable to save");
		printf("\n");
	    }
	    else
	    {
		&print_user("saved");
	    }
	}
	else
	{
	    &print_user("ok");
	}

	$userDB->DESTROY();
	$userSYS->DESTROY();
    }

    return;
}

# build_user_system ()
#
#   tries to build a Meta object of type "users"
#   from information gathered by the system
#
#   arg:  login name
#   ret:  Meta object (type => users)
#
sub build_user_system ()
{
    $User = shift;
    return unless ($User);

    my (@tmpARRAY);
    my $USER = Cobalt::Meta->new("type" => "users");

    # get basic user info
    my ($name,$uid,$fullname,$dir,$sh) = (getpwnam("$User"))[0,2,6,7,8];
    unless ($name)
    {
	&print_user_error("does not exist in password file");
	return;
    }

    # determine virtual site from home directory path
    my $vsite;
    if ($dir =~ m|^/home/sites/home/users/|)
    {
	$vsite = "home";
    }
    elsif ($dir =~ m|^/home/sites/(site[0-9]+)/users/|)
    {
	$vsite = "$1";
    }
    else
    {
	&print_user_error("invalid home directory");
	return;
    }

    printf("\n") if ($Verbose);

    # user name
    $USER->put("name", "$name");
    &print_field("user name", $name) if ($Verbose);

    # full name
    $USER->put("fullname", "$fullname");
    $USER->put("altname", "");
    &print_field("full name", $fullname) if ($Verbose);

    # UID
    $USER->put("uid", "$uid");
    &print_field("UID", $uid) if ($Verbose);

    # virtual site membership
    $USER->put("vsite", "$vsite");
    &print_field("virtual site", $vsite) if ($Verbose);

    # disk quota
    my $quota = 0;
    my $blkquota = (Cobalt::Quota::repquota("$User"))[0,1];
    if ($blkquota > 0)
    {
	$quota = int(($blkquota * 1024) / 1048576);
    }
    $USER->put("quota", "$quota");
    &print_field("disk quota", $quota) if ($Verbose);

    # email aliases
    my $aliases = "";
    @tmpARRAY = (Cobalt::Email::mail_virtuser_get_byuser("$User"));
    if (scalar(@tmpARRAY))
    {
	# remove domains
	map { $_ =~ s/^([^@]+)\@.*/$1/; } @tmpARRAY;
	$aliases = join(" ", @tmpARRAY);
    }
    $USER->put("aliases", "$aliases");
    &print_field("aliases", $aliases) if ($Verbose);

    # email forwarding
    my $forward = "off";
    @tmpARRAY = (Cobalt::List::alias_get_vacationless("$User"));
    if (scalar(@tmpARRAY))
    {
	$forward = join(" ", @tmpARRAY);
    }
    $USER->put("forward", "$forward");
    &print_field("forward", $forward) if ($Verbose);

    # virtual site administrator
    @tmpARRAY = (Cobalt::User::user_list_groups("$User"));
    my $admin = (scalar(grep(/\b$vsite\b/, @tmpARRAY))) ? "on" : "off";
    $USER->put("admin", "$admin");
    &print_field("site admin", $admin) if ($Verbose);

    # suspended user
    my $suspend = (Cobalt::User::user_issuspend("$User")) ? "on" : "off";
    $USER->put("suspend", "$suspend");
    &print_field("suspended", $suspend) if ($Verbose);

    # telnet/shell access
    my $shell = ($sh eq "/bin/bash") ? "on" : "off";
    $USER->put("shell", "$shell");
    &print_field("shell", $shell) if ($Verbose);

    # authenticated pop3 (APOP)
    my $apop = (Cobalt::Email::mail_apop_isuser("$User")) ? "on" : "off";
    $USER->put("apop", "$apop");
    &print_field("secure POP3", $apop) if ($Verbose);

    # frontpage 2000 extensions
    @tmpARRAY = Cobalt::Vsite::vsite_get_fpx($vsite);
    my $fpx = (scalar(grep(/\b$User\b/, @tmpARRAY))) ? "on" : "off";
    $USER->put("fpx", "$fpx");
    &print_field("frontpage", $fpx) if ($Verbose);

    # email vacation responder
    my $vacation = (Cobalt::Vacation::vacation_get_on("$User")) ? "on" : "off";
    $USER->put("vacation", "$vacation");
    my $vacationmsg = (Cobalt::Vacation::vacation_get_message("$User"));
    $USER->put("vacationmsg", "$vacationmsg");

    if ($Verbose)
    {
	&print_field("vacation", $vacation);
	if ($vacation eq "on")
	{
	    $vacationmsg =~ tr/\012\015/ /;
	    &print_field("vacation msg", $vacationmsg);
	}
    }

    printf("\n") if ($Verbose);

    return $USER;
}






