#!/usr/bin/perl
#
#
$BENNO_USERADMIN::VERSION = '2.8.7';

use strict;
use Carp;
use Getopt::Std;
use DBI;
use Digest::MD5;
use lib qw(/usr/share/benno-web);
use Passwords;
use IPC::Open3;
use File::Basename;

my $DEBUG = $ENV{DEBUG};

our(%opts);
getopts('haDdElLstvVC:c:e:m:M:n:p:r:u:', \%opts);

my $help        = $opts{'h'};
my $list        = $opts{'l'};
my $configfile  = $opts{'c'} || '/etc/benno-web/benno.conf';
my $config;
eval {
    $config = read_config($configfile);
};
if ($@) {
    print STDERR "$@\n";
    print_help();
    exit 2;
}

my $disable     = $opts{'d'};
my $enable      = $opts{'E'};
my $delete      = $opts{'D'};

my $password    = $opts{'p'};
my $uid         = $opts{'u'};
my $name        = $opts{'n'};
my $email       = $opts{'e'};

my $role        = $opts{'r'};
my $listauth    = $opts{'L'};
my $modname     = $opts{'M'};
my $testauth    = $opts{'t'};
my $authmodule  = $opts{'m'};
my $container   = $opts{'C'};

my $version     = $opts{'v'};
my $verbose     = $opts{'V'};

$config->{module_dir} = $config->{module_dir} || '/usr/lib/benno-web';


$version && print_version();
$help    && print_help()     &&  exit;

$listauth && list_authmodules($config)           && exit;
$modname  && change_authmodule($config,$modname) && exit;

my $DBH = db_connect($config);

# no option
if (! $list &! $uid) {
    print_help();
    exit 1;
}

my $exit_cmd = 1;
if ($password || $name || $email || $container || $role) {
    $exit_cmd = 0; 
}

$testauth   && test_auth($config,$authmodule,$uid,$password)    && exit;
$list       && $verbose && list_users_full()                    && exit; 
$list       && list_users()                                     && exit; 
$enable     && disable_user($uid,0) && $exit_cmd                && exit;
$disable    && disable_user($uid,1) && $exit_cmd                && exit;
$delete     && delete_user($uid)                                && exit;

if ($password || $name || $email || $role || $container) {
    # change or add user
    eval {
        change_user($uid,$password,$name,$email,$role,$container);
    };
    if ($@) {
        add_user($uid,$password,$name,$email,$role,$container,$disable);
    }
    exit;
}
else {
    # show user details
    show_user($uid);
    exit;
}

print_help();
exit 0;

### SUBS #######################################################################
# list_users
sub list_users
{
    my $sql = 'SELECT id FROM user';

    my $sth = $DBH->prepare($sql);
    my $rv  = $sth->execute;
    while (my @row = $sth->fetchrow_array) {
        print "$row[0]\n";
    }
    return 1;
}


# list_users_full
sub list_users_full
{
    my $sql = 'SELECT id FROM user';

    my $sth = $DBH->prepare($sql);
    my $rv  = $sth->execute;
    while (my @row = $sth->fetchrow_array) {
        print '-'x72,"\n";
        show_user($row[0]);
    }

    return 1;
}


# show_user
sub show_user
{
    my $uid = shift;
    my @user = fetch_user($uid); 
    ($#user == 4) or error_exit('User not found.');

    my $containers;
    my @containers = fetch_container($uid);
    foreach my $container (@containers) {
        $containers .= $container.',';
    }
    $containers =~ s/,$//;

    print "Userid:    $user[0]\n";
    print "Name:      $user[1]\n";
    print "Container: $containers\n";
    print "Role:      $user[4]\n";
    print "E-Mail:    ";
    my $adresslist;
    foreach my $address (fetch_adresses($uid)) {
        $adresslist .= $address.', ';
    }
    $adresslist =~ s/, $//;
    print "$adresslist\n";

    if ($user[2] =~ /^DISABLED_/) {
        print "Enabled:   NO\n"
    }
}


# add_user
sub add_user
{
    my $uid       = shift;
    my $pass      = shift;
    my $name      = shift;
    my $email     = shift;
    my $role      = shift || 'USER';
    my $container = shift || 'BennoContainer';
    my $disable   = shift;

    # check if user exists
    my @user = fetch_user($uid); 
    ($#user == 4) && error_exit('User alredy exists.');
    $role eq 'ADMIN' || $role eq 'REVISOR' || $email || error_exit('No email address / filter for user given.');

    # first, insert user
    my $sql = 'INSERT INTO user (id,archive,role) VALUES (?,?,?)';
    my $rows = $DBH->do($sql,undef,$uid,$container,$role) or error_exit($DBH->errstr);

    $pass   && update_password($uid,$pass,$disable);
    $name   && update_name($uid,$name);
    $email  && update_email($uid,$email);
    $container && update_container($uid,$container);
}


# change_user
sub change_user
{
    my $uid       = shift;
    my $pass      = shift;
    my $name      = shift;
    my $email     = shift;
    my $new_role  = shift;
    my $container = shift;

    # check if user exists
    my @user = fetch_user($uid); 
    ($#user == 4) || die "User does not exist.\n";
    $role = $new_role || $user[4];
    $role eq 'ADMIN' || $role eq 'REVISOR' || $email || error_exit('No email address / filter for user given.');

    $pass   && update_password($uid,$pass,$disable);
    $name   && update_name($uid,$name);
    $email  && update_email($uid,$email);
    $role   && update_role($uid,$role);
    $container && update_container($uid,$container);
}


# delete_user
sub delete_user
{
    my $uid  = shift;

    # check if user exists
    my @user = fetch_user($uid); 
    ($#user == 4) || error_exit('User does not exist.');

    # first, insert user
    my $sql = 'DELETE FROM user WHERE id=?';
    my $rows = $DBH->do($sql,undef,$uid) or error_exit($DBH->errstr);

    $sql = 'DELETE FROM address WHERE id=?';
    $rows = $DBH->do($sql,undef,$uid) or error_exit($DBH->errstr);

    $sql = 'DELETE FROM storedquery WHERE userid=?';
    $rows = $DBH->do($sql,undef,$uid) or error_exit($DBH->errstr);

    $sql = 'DELETE FROM filter WHERE userid=?';
    $rows = $DBH->do($sql,undef,$uid) or error_exit($DBH->errstr);

    $sql = 'DELETE FROM container WHERE userid=?';
    $rows = $DBH->do($sql,undef,$uid) or error_exit($DBH->errstr);
}


# list_authmodules
sub change_authmodule
{
    my ($config,$modname) = @_;

    # read module dir
    my $auth_dir = $config->{'AUTH_DIR'} || '/etc/benno-web/auth.d';
    my $mod_dir  = $config->{'AUTHMODULE_DIR'} || '/usr/lib/benno-web';

    my $authmodule = "$auth_dir/$modname";
    my $basemodule = "$mod_dir/$modname";

    if (-l "$auth_dir/$modname") {
        unlink $authmodule;
    }
    else {
print "symlink $basemodule, $authmodule\n";
        symlink $basemodule, $authmodule;
    }

    list_authmodules($config);

    return 1;
}


# list_authmodules
sub list_authmodules
{
    my ($config) = @_;
    my $modules = {};

    # read module dir
    my $mod_dir = $config->{'AUTHMODULE_DIR'} || '/usr/lib/benno-web';
    opendir (DIR, $mod_dir) or croak "Cannot read directory: $!";
    while (my $modfile = readdir(DIR)) {
        next if $modfile =~ /^\./;
        $modfile = "$mod_dir/$modfile";
        next unless -x $modfile;
        $modules->{$modfile}{name} = basename($modfile);
        $modules->{$modfile}{status} = '';
    }
    my $auth_dir = $config->{'AUTH_DIR'} || '/etc/benno-web/auth.d';
    opendir (DIR, $auth_dir) or croak "Cannot read directory: $!";
    while (my $modfile = readdir(DIR)) {
        next if $modfile =~ /^\./;
        $modfile = "$auth_dir/$modfile";
        if (-l $modfile) {
            my $basefile = readlink($modfile);
            next unless -x $basefile;
            $modules->{$basefile}{name} = basename($basefile);
            $modules->{$basefile}{status} = 'ACTIVE';
        }
        else {
            $modules->{$modfile}{name} = basename($modfile) .' (custom)';
            if (-x $modfile) { $modules->{$modfile}{status} = 'ACTIVE'; }
            else { $modules->{$modfile}{status} = 'NOTEXEC'; }
        }
    }
    foreach my $modfile (sort keys %{$modules}) {
        if ($modules->{$modfile}->{status} eq 'ACTIVE') { print '* ';}
        else { print '  ';}
        print "$modules->{$modfile}->{name}\n";
    }

    return 1;
}


# test_auth
sub test_auth {
    my ($config,$module,$user,$password) = @_;

    unless ($password) {
        $password = read_password();
    }

    $module or $module = '/usr/sbin/benno_auth.d';
    unless (-e $module) {
        $module = $config->{module_dir}.'/'.$module;
    }
    print "[DEBUG] Test AUTH by $module\n" if $DEBUG;

    my $authstring = "$user\n$password";
    my($wtr, $rdr, $err);
    use Symbol 'gensym'; $err = gensym;
    my $pid = open3($wtr, $rdr, $err, $module);
    print $wtr $authstring;
    close $wtr;
    waitpid( $pid, 0 );
    my $child_exit_status = $? >> 8;
    my @err = <$err>;
    foreach my $line (@err) {
        my ($basename) = $module =~ m!.*?([^/]+?)$!;
        print $line;
    }
    my @ret = <$rdr>;
    foreach my $line (@ret) {
        print $line;
    }

    return 1;
}



# update_password
sub update_password
{
    my $uid  = shift;
    my $pass = shift;
    my $disable = shift;

    if ($pass eq '-') {
        $pass = read_password();
    }

    my $pwdigest = password_hash($pass,PASSWORD_BCRYPT,('cost' => 10));
    $pwdigest = 'DISABLED_'.$pwdigest if $disable;

    my $sql = 'UPDATE user SET password=? WHERE id=?';
    my $rows = $DBH->do($sql,undef,$pwdigest,$uid) or error_exit($DBH->errstr);
}


sub read_password
{
    my ($password) = @_;
    eval {
        require Term::ReadKey;
    };
    if ($@) {
        # no module available;
        print STDERR "No password given: -p <password>\n>>>$@\n";
        exit 5;
    }
    else {
        Term::ReadKey::ReadMode('noecho');
        print "Password: ";
        $password = Term::ReadKey::ReadLine(0);
        Term::ReadKey::ReadMode('restore');
        print "\n";
        $password =~ s/\R\z//;
    }

    return $password;
}


# update_name
sub update_name
{
    my $uid  = shift;
    my $name = shift;

    my $sql = 'UPDATE user SET name=? WHERE id=?';
    my $rows = $DBH->do($sql,undef,$name,$uid) or error_exit($DBH->errstr);
}


# update_email
sub update_email
{
    my $uid  = shift;
    my $email = shift;
    my $sql;

    my @addresslist = split(/,/,$email);

    local $DBH->{RaiseError} = 0;
    local $DBH->{PrintError} = 0;

    $DBH->begin_work;       # start transaction
    $sql = 'DELETE FROM address WHERE id=?';
    my $rows = $DBH->do($sql,undef,$uid) or error_exit($DBH->errstr);

    $sql = 'INSERT INTO address (id,address) VALUES(?,?)';
    my $sth = $DBH->prepare($sql);
    foreach my $address (@addresslist) {
        $address =~ s/\s//g;
        my $rv  = $sth->execute($uid,$address);
    }
    $DBH->commit;       # end transaction
}


# update_role
sub update_role
{
    my $uid  = shift;
    my $role = shift;

    my $sql = 'UPDATE user SET role=? WHERE id=?';
    my $rows = $DBH->do($sql,undef,$role,$uid) or error_exit($DBH->errstr);
}


# update_container
sub update_container
{
    my $uid       = shift;
    my $container = shift;
    # deprecated
    my $sql = 'UPDATE user SET archive=? WHERE id=?';
    my $rows = $DBH->do($sql,undef,$container,$uid) or error_exit($DBH->errstr);

    # >= 2.8.4
    my @containers = split /,\s*/, $container;
    my $sql = 'DELETE FROM container WHERE userid=?';
    my $rows = $DBH->do($sql,undef,$uid) or error_exit($DBH->errstr);
    foreach my $cnt (@containers) {
        my ($cid,$scid) = split(/\//,$cnt);
        $scid = '' unless $scid;
        $DBH->do(q{INSERT INTO container (userid,cid,scid) VALUES (?,?,?)},
            undef, $uid,$cid,$scid);
    }
}


# enable_user(1|0)
sub disable_user
{
    my $uid     = shift;
    my $disable = shift;

    my $sql = 'SELECT password FROM user WHERE id = ?';
    my $sth = $DBH->prepare($sql);
    my $rv  = $sth->execute($uid);
    my $pwd = ($sth->fetchrow_array)[0];
    if ($disable) {
        $pwd = 'DISABLED_'.$pwd;
    }
    else {
        $pwd =~ s/^DISABLED_//;
    }
    $sql = 'UPDATE user SET password=? WHERE id=?';
    my $rows = $DBH->do($sql,undef,$pwd,$uid) or error_exit($DBH->errstr);
}


# fetch_user
sub fetch_user
{
    my $uid = shift;

    my $sql = 'SELECT * FROM user WHERE id = ?';

    my $sth = $DBH->prepare($sql);
    my $rv  = $sth->execute($uid);
    my @user = $sth->fetchrow_array;

    return @user;
}


# fetch_container
sub fetch_container
{
    my $uid = shift;
    my @containers;

    my $sql = 'SELECT cid FROM container WHERE userid = ?';

    my $sth = $DBH->prepare($sql);
    my $rv  = $sth->execute($uid);
    while (my $container = $sth->fetchrow_array) {
        push @containers, $container;
    }
    return @containers;
}


# fetch_adresses
sub fetch_adresses
{
    my $uid = shift;

    my @adresses;

    my $sql = 'SELECT address FROM address WHERE id = ?';
    my $sth = $DBH->prepare($sql);
    my $rv  = $sth->execute($uid);
    while (my @row = $sth->fetchrow_array) {
        push @adresses,$row[0];
    }

    return @adresses;
}


# db_connect
sub db_connect
{   
    my $config = shift;

    my $dbtype = $config->{DBTYPE} || 'sqlite:////var/lib/benno-web/bennoweb.sqlite';
    my $db     = $config->{DATABASE};
    my $dbhost = $config->{DBHOST} || 'localhost';
    my $dbport = $config->{DBPORT} || 3306;
    my $dbuser = $config->{DBUSER};
    my $dbpass = $config->{DBPASS};
    my $DBH;
    if ($dbtype eq 'mysql') {
        eval {
            my $dsn = "DBI:mysql:database=$db;host=$dbhost;port=$dbport";
            $DBH = DBI->connect($dsn,$dbuser,$dbpass,{mysql_enable_utf8 => 1});
        };
        if ($@) {
            print STDERR "ERROR: MySQL Driver not installed.\n";
            exit 1;
        }
    }
    else {
        if ($dbtype =~ /^sqlite:\/\/(\/.\S+)$/) {
            my $dsn = "dbi:SQLite:dbname=$1","","";
            $DBH = DBI->connect($dsn);
        }
    }
    return $DBH;
}


# error_exit
sub error_exit
{
    my $err = shift;
    print STDERR "$err\n";
    exit 1;
}

# print_version
sub print_version
{
    print "benno-useradmin version $BENNO_USERADMIN::VERSION\n";
    exit 0;
}


# read_config
sub read_config
{
    my $configfile = shift;

    my $config = {};
    open CONF, $configfile or die "Cannot open config file $configfile. $!\n";
    foreach my $line (<CONF>) {
        next if $line =~ /^$/;
        next if$line  =~ /^#/;
        chomp $line;
        my ($param,$value) = split(/\s*=\s*/,$line,2);
        $config->{$param} = $value;
    }

    return $config;
}


# print_help
sub print_help
{
    print "Usage: $0 [-h] [-l] [-u <username>] [-d|-E] [-D] [-p <password>]\n";
    print "  [-n <name>] [-e <email1,email2, ...>] -C [<container>]\n";
    print "\n";
    print "  -c <file>          config file (default /etc/benno-web/benno.conf)\n";
    print "  -l                 list all users\n";
    print "  -L                 list all authmodules\n";
    print "  -M <modulename>    enable/disable authmodule\n";
    print "\n";
    print "  -u <username>      username (shows user data if no other options given)\n";
    print "\n";
    print "\n";
    print "Add or change user data\n";
    print "  -p <password|->    password of the user (- ask for password on stdin)\n";
    print "  -n <name>          display name of the user\n";
    print "  -e <email,email>   comma separated list of email addresses ('' deletes all)\n";
    print "  -r <role>          user role (USER, ADMIN, REVISOR, default USER)\n";
    print "  -C <container>     container name (default BennoContainer)\n";
    print "\n";
    print "  -d                 disable user\n";
    print "  -E                 enable user\n";
    print "  -D                 delete user\n";
    print "  -t                 Test authentication for user give with -u\n";
    print "  -m <authmodule>    Test authentication with module (-m benno_ldapauth)\n";
    print "  -h                 this help\n";
    print "  -v                 print version number\n";
    print "  -V                 verbose output\n";
    print "\n";
}


