#!/usr/bin/perl

use strict;
use utf8;
use warnings;

use Fcntl qw(LOCK_EX LOCK_UN O_CREAT O_RDWR);
use FindBin ();    # depends: perl
use Getopt::Long qw(GetOptions);             # depends: perl
use Time::ParseDate qw(parsedate);           # depends: libtime-modules-perl
use Data::Dump qw(pp);                       # depends: libdata-dump-perl
use Log::Log4perl qw(get_logger :levels);    # depends: liblog-log4perl-perl
use noris::IO::AutoEncoding;                 # depends: libnoris-io-autoencoding-perl

use noris::Log::Dispatch::SyslogAppender;    # depends: libnoris-log-dispatch-syslogappender-perl

sub my_parsedate($) {
    my ($datum) = @_;
    defined( my $epoch = parsedate( $datum, PREFER_PAST => 1, WHOLE => 1 ) )
      or die(qq(I do not understand the date '$datum'!));
    $epoch;
}

my ( %FrontendArg, %BackendArg, $DeleteUpto );
GetOptions(
    'backend=s'        => \my $BackendModule,
    'backend-arg=s%'   => \%BackendArg,
    'backend-config=s' => sub {
        ( undef, my $file ) = @_;
        require Config::Simple;    # depends libconfig-simple-perl
        Config::Simple->import_from( $file, \%BackendArg )
          or die(
            'Cannot read configuration from file: ' . Config::Simple->error );
    },
    'frontend-config=s' => sub {
        ( undef, my $file ) = @_;
        require Config::Simple;    # depends libconfig-simple-perl
        Config::Simple->import_from( $file, \%FrontendArg )
          or die(
            'Cannot read configuration from file: ' . Config::Simple->error );
    },
    'debug!' => \( my $Debug = 0 ),
    'delete-list-file=s' => \my $DeleteListFile,
    'delete-only'	 => \my $DeleteOnly,
    'delete-upto=s'      => sub {
        my ( $option, $value ) = @_;
        $DeleteUpto = my_parsedate($value);
        die("-$option does not accept Time in future.\n")
          if $DeleteUpto > time;
    },
    'help|?' => sub {
        exec perldoc => -F => $0
          or die("Cannot execute perldoc: $!");
    },
    'log-config=s'  => \my $LogConfig,
    'max-changes=i' => \( my $MaxChanges = 42 ),
) or exit 1;

# Logging
if ( defined $LogConfig ) {
    die "Log-Configfile $LogConfig doesn't exist"  unless -e $LogConfig;
    Log::Log4perl->init_and_watch($LogConfig, "HUP");

    no warnings 'once';
    binmode SYSLOG, ":utf8";
}
else {
    # Konfiguration für Testserver auf Kommandozeile:
    # Nur Probleme ausgeben
    Log::Log4perl->init(\<<EOT);
        log4perl.logger = INFO, debug
        log4perl.appender.debug = Log::Log4perl::Appender::Screen
        log4perl.appender.debug.stderr  = 1
        log4perl.appender.debug.utf8 = 1
        log4perl.appender.debug.layout = Log::Log4perl::Layout::PatternLayout
        log4perl.appender.debug.layout.ConversionPattern = %c pid=%P %x: %m%n
EOT
}

my $logger = get_logger("ManageUsers");

# Backend Modul laden
unless ( defined $BackendModule ) {
    $logger->logdie("Backendmodule not defined!");
}

eval "require $BackendModule";
$logger->logdie("Cannot load Backend: $@") if length($@);

# HAUPT PROGRAMM
# Übrige Variablen definieren
unless ( defined $DeleteListFile ) {
    $DeleteListFile = "/var/lib/$FindBin::Script/$BackendModule";
}

# Start des Scripts
my $backend = $BackendModule->new(%BackendArg);

my $countChanges = 0;
my ( %solluser, %istuser, %deluser, @tocreate, @todelete );

# Soll-User einlesen
{
    chomp( my @readuser = <> );
    @solluser{@readuser} = ();
}

# Vom Backend die Ist-User einlesen
@istuser{ $backend->list_users() } = ();

# Vom DeleteListFile die zu löschenden User einlesen
$logger->debug("Reading DeleteListFile File: $DeleteListFile");
sysopen my $lock_fh, $DeleteListFile, O_CREAT | O_RDWR
  or $logger->logdie(qq(Cannot open '$DeleteListFile': $!\n));
flock $lock_fh, LOCK_EX
  or $logger->logdie(qq(Cannot lock '$DeleteListFile': $!\n));

while ( defined( my $delline = <$lock_fh> ) ) {

    # chomp() überflüssig, weil weiter hinten eh noch ein Kommentar folgt:
    my @delline = split /\t/, $delline;
    $deluser{ $delline[0] } = $delline[1];
}

# Nachschauen welche User erzeugt und ob welche
# wieder reaktiviert werden müssen
foreach my $soll ( keys %solluser ) {
    if ( exists $deluser{$soll} ) {
        delete $deluser{$soll};
        $countChanges++;
        $logger->info(
"Deleting User '$soll' from the DeleteFile, because he has been reactivated."
        );
    }
    if ( exists $istuser{$soll} ) {
        delete $istuser{$soll};
    }
    elsif ( !$DeleteOnly && grep /^$soll$/, grep defined($_), $FrontendArg{'ignoreuser'} ) {
        $logger->debug("IGNORE: Adding User '$soll' to Create-List.");
    }
    elsif ( !$DeleteOnly ) {
        push @tocreate, $soll if ($soll);
        $logger->info("Adding User '$soll' to Create-List.");
    }
}
$logger->debug( 'Create-User List: ' . pp(@tocreate) );

# Nachschauen welche User endgültig gelöscht
# und welche User zum Löschen markiert werden müssen
foreach my $ist ( keys %istuser ) {
    if ( grep /^$ist$/, grep defined($_), $FrontendArg{'ignoreuser'} ) {
        $logger->debug("IGNORE: User '$ist' wont be added for Deletion.");
        next;
    }
    unless ( exists $deluser{$ist} ) {
        $deluser{$ist} = $^T;
        $countChanges++;
        $logger->info("User '$ist' added to DeleteFile.");
    }
    if ( defined $DeleteUpto && $deluser{$ist} <= $DeleteUpto ) {
        push @todelete, $ist;
        delete $deluser{$ist};
        $logger->info("User '$ist' added to Deletion-List.");
    }
}
$logger->debug( 'Delete User List: ' . pp(@todelete) );

# Überprüfen ob zuviele Änderungen durchgeführt werden müssen
$countChanges += @tocreate unless $DeleteOnly;
$countChanges += @todelete;
if ( $countChanges > $MaxChanges ) {
    flock( $lock_fh, LOCK_UN )
      or $logger->logdie("Error on unlocking the Lock-File: $!");
    close $lock_fh
      or $logger->logdie("Error on closing the Lock-File: $!");
    $logger->logdie(
qq(I have to do '$countChanges' Actions, but there are only '$MaxChanges' allowed!)
    );
}
$logger->debug("Number of Actions that have to be done: $countChanges");

# User die noch nicht vorhanden sind erzeugen.
unless ($DeleteOnly) {
    foreach my $mk (@tocreate) {
        if ( $BackendArg{ignoreuser} && grep /^$mk$/, $BackendArg{ignoreuser} ) {
            $logger->info("IGNORE: User '$mk' wont be created.");
            next;
        }
        $logger->info("User '$mk' is being created.");
        $backend->add_user($mk);
    }
}

# User endgültig löschen
foreach my $rm (@todelete) {
    if ( $BackendArg{ignoreuser} && grep /^$rm$/, $BackendArg{ignoreuser} ) {
        $logger->debug("IGNORE: User $rm is beeing deleted.");
        next;
    }
    $logger->info("User '$rm' is beeing deleted.");
    $backend->delete_user($rm);
    delete $deluser{$rm};
}

# Das neue DeleteUserList file schreiben.
$logger->debug("Writing the new Delete-User-File$DeleteListFile");
seek $lock_fh, 0, 0
  or $logger->logdie("ERROR: On Seek 0 of the Delete-User-File: $!");
truncate $lock_fh, 0
  or $logger->logdie("ERROR: On truncating the Delete-User-File: $!");
foreach my $df ( keys %deluser ) {
    print $lock_fh "$df\t$deluser{$df}\t# " . localtime( $deluser{$df} ) . "\n";
}

flock( $lock_fh, LOCK_UN )
  or $logger->logdie("ERROR: On unlocking the Lock-File: $!");
close $lock_fh or $logger->logdie("ERROR: On closing the Lock-File: $!");

__END__

=head1 NAME

manage-users - module based Account-Management

=head1 SYNOPSE

    sed 's/:.*//' DATEI |
    manage-users -max-changes 24 \
                 -delete-list-file /path/to/file \
                 -delete-upto '6 weeks ago' \
                 -backend noris::ManageUsers::Cyrus \
                 -backend-config /path/to/file \
                 -frontend-config /path/to/file \
                 -backend-arg foo=bar

Alternativ:

    manage-users -max-changes 10 \
                 -delete-list-file /path/to/file \
                 -delete-upto '-6 weeks' \
                 -backend noris::ManageUsers::Cyrus \
                 -backend-arg foo=bar \
                 -backend-arg bla=fasel \
                 DATEI

Mit DATEI ist eine Datei gemeint die eine Liste mit Usernamen (einer pro Zeile)
enthält. Alternativ dazu kann man über die Standardeingabe eine Liste der
aktuell vorzusehenden Usernamen (einer pro Zeile) verfüttern. Im Anschluss
entscheidet es, welche User angelegt und welche gelöscht werden müssen.

=head1 BESCHREIBUNG

Dieses Programm kann automatisiert Accounts bearbeiten, anlegen, löschen etc.
Das Programm ist unabhängig vom jeweilig benutzten Server da dieser über die
verschiedenen Module angesprochen wird.

=head1 OPTIONEN

=over 4

=item -help

=item -?

um (nur) diese Dokumentation anzeigen zu lassen

=item -max-changes Integer

Überschreiten die benötigten Änderungen den Wert der angegeben ist. Führt es
vorsichtshalber keine Änderungen durch.

Default: C<42>, sprich: "max. 42 Änderungen."

=item -delete-list-file Dateiname

Datei, in dem das Programm sich merken soll, welche User wann gelöscht werden
sollen.

Default: C</var/lib/I<$FindBin::Script>/I<$BackendModule>>

=item -delete-upto Datum

Lösche Accounts, die schon mindestens seit dem angegebenen Datum zum Löschen
vorgemerkt sind.

Als Datum kann dabei alles angegeben werden, was L<Time::Parsedate/parsedate>
versteht.

Default: Es wird gar nichts gelöscht.

=item -backend Perl-Modul

Hier wird das Backendmodul für den Server angegeben.

=item -backend-arg ARG=WERT

=item -backend-config FILE

um Konfigurationswerte für das Backend-Modul auf der Kommandozeile bzw.
über eine Konfigurationsdatei anzugeben.

Die beiden Mechanismen können nach Belieben kombiniert werden.
Wird mehrfach dieselbe Variable definiert, so gewinnt die letzte Angabe.

Beispiele:
    -backend-arg foo=bar -backend-arg bla=fasel
    -backend-config /etc/ManageUsers/Cyrus.conf

=item -debug

Falls Debug meldungen erwünscht sind.

=item -frontend-config FILE

um Konfigurationswerte für das Frontend-Modul auf der Kommandozeile bzw.
über eine Konfigurationsdatei anzugeben.

Beispiele:
    -frontend-config /etc/ManageUsers/Frontend.conf

=item -delete-only

Lösche nur User, führe keine anderen Aktionen durch.

=item -help

=item -?

um (nur) diese Dokumentation anzeigen zu lassen

=back

=head1 NOTWENDIGE BACKEND METHODEN

Damit manage-users funktioniert müssen im Backendmodul bestimmte Methoden
vorhanden sein.

=over 4

=item new()

Diese Funktion wird als erstes aufgerufen und der werden alle notwendigen
Parameter übergeben.

    use Backend::Module;

    my %BackendArd = ('foo' => 'bar', 'bla' => 'fasel',);
    my $BackendObject = $Backend::Module->new(%BackendArg);

=item list_users()

list_users muss eine Liste mit allen vorhandenen Usern zurückliefern

    my @user_list = $BackendObject->list_users()

=item add_user()

add_user wird der Username des zu generierenden Benutzers übergeben
Für den Rest ist das Backendmodul zuständig. z.B. Subfolder generierung,
Rechtevergabe intern etc.

    $BackendObject->add_user('username');

=item delete_user()

delete_user wird der Username des zu löschenden Benutzers übergeben.
Für den Rest ist das Backendmodul zuständig. z.B. Subfolder löschung,
Benutzer löschen, Rechtevergabe intern etc.

    $BackendObject->delete_user('username');

=back

=head1 AUTOR

 Stelios Gikas <stelios.gikas@noris.net>
 Stelios Gikas <303717@ticket.noris.net>

