#!/usr/bin/env perl

use v5.10.1;
use strict;
use warnings;

use Digest::SHA1;
use IPC::Cmd qw[run];
use MIME::Base64;
use Net::LDAPS;
use Net::LDAP::Util qw(ldap_error_text);
use Pod::Usage;
use Term::ANSIColor qw(:constants);
use Term::ReadKey;

#use Cwd 'abs_path';
#use File::Basename;
#use File::Find;

# Avoid installation of liblerdorf on workstations
use lib "/sgoinfre/root/new_intra/";

use ACU::LDAP;
use ACU::Log;

###########################################################
#                                                         #
#                    Global variables                     #
#                                                         #
###########################################################

my $wksHomePrefix = "/home/";
my $nfsHomePrefix = "/srv/nfs/accounts/";

my $shellValid = "/bin/zsh";

my $colorize = defined($ENV{'ENABLE_COLOR'});

my %dev_quota = ( home => "/dev/mapper/acu-nfs--accounts",
		  sgoinfre => "/dev/mapper/acu-nfs--sgoinfre" );
my %def_quota = ( block => { home => 2306866, sgoinfre => 5242880 },
		  file => { home => 50000, sgoinfre => 60000 } );

###########################################################
#                                                         #
#                       Main Program                      #
#                                                         #
###########################################################

my $dbh;

my %cmds =
(
  "account"	=> \&cmd_account,
  "group"	=> \&cmd_group,
  "help"	=> \&cmd_help,
  "list"	=> \&cmd_list,
);

my %cmds_account =
(
  "alias"	  => \&cmd_account_alias,
  "close"	  => \&cmd_account_close,
  "cn"		  => \&cmd_account_cn,
  "create"	  => \&cmd_account_create,
  "finger"	  => \&cmd_account_view,
  "mail"	  => \&cmd_account_mail,
  "name"	  => \&cmd_account_cn,
  "nopass"	  => \&cmd_account_nopass,
  "password"	  => \&cmd_account_password,
  "passgen"	  => \&cmd_account_passgen,
  "photo"	  => \&cmd_account_photo,
  "quota"         => \&cmd_account_quota,
  "reopen"        => \&cmd_account_reopen,
  "rights"        => \&cmd_account_rights,
  "services"	  => \&cmd_account_services,
  "shell"         => \&cmd_account_shell,
  "view"	  => \&cmd_account_view,
  "view"	  => \&cmd_account_view,

  "grant-intra"	  => \&cmd_account_grantintra,
  "grant-lab"	  => \&cmd_account_grantlab,
  "grant-mail"	  => \&cmd_account_grantmail,
);

my %cmds_group =
(
  "list"	=> \&cmd_group_list,
  "add"		=> \&cmd_group_add,
  "remove"	=> \&cmd_group_remove,
  "create"	=> \&cmd_group_create,
  "delete"	=> \&cmd_group_delete
);

my %cmds_list =
(
  "accounts"	=> \&cmd_list_accounts,
  "groups"	=> \&cmd_list_groups,
  "roles"	=> \&cmd_list_roles,
);


######################################
#                                    #
#   UTILITY FUNCTIONS		     #
#                                    #
######################################

sub ldap_get_password()
{
    my $bindsecret;
    if (defined($ENV{'LDAP_PASSWORD'}) && $ENV{'LDAP_PASSWORD'} ne "") {
	return $ENV{'LDAP_PASSWORD'};
    }

    say "To avoid typing password everytime, set LDAP_PASSWORD in your env.";
    say "Do not do this in your shell configuration file!";
    say "Use a command like:\n";
    say '  $ echo -n "LDAP password: "; read -s LDAP_PASSWORD; echo';
    say '  $ LDAP_PASSWORD=$LDAP_PASSWORD lpt ...';
    say "The last line prevent you from exporting the LDAP password to all commands but lpt!";
    say "";

    ReadMode("noecho");
    print BOLD, "Need LDAP password: ", RESET;
    $bindsecret = <STDIN>;
    ReadMode("restore");
    print "\n";

    chomp $bindsecret;
    return $bindsecret;
}

$LDAP::binddn = "cn=admin,dc=acu,dc=epita,dc=fr";
$LDAP::secret_search = \&ldap_get_password;

######################################
#                                    #
#   ACCOUNT BLOCK		     #
#                                    #
######################################

sub cmd_account(@)
{
    my $login = shift;

    if (! $login) {
	log(USAGE, "lpt account <login> <command> [arguments ...]");
	return 1;
    }

    my $subcmd = shift // "view";

    if (! $subcmd) {
	pod2usage(-verbose => 99,
		  -sections => [ 'ACCOUNT COMMANDS' ] );
    }
    elsif (! exists $cmds_account{$subcmd}) {
	log(USAGE, "Unknown command for account: ". $subcmd);
	return 1;
    }

    return $cmds_account{$subcmd}($login, @_);
}

sub cmd_account_alias($@)
{
    return cmd_account_multiple_vieworchange('mailAlias', 'alias', @_);
}

sub cmd_account_close($@)
{
    my $login = shift;

    if ($#_ > -1) {
	log(USAGE, "<lpt> account <login> close");
	return -1;
    }

    my $ldap = LDAP::ldap_connect();

    my $mesg = $ldap->search( # search
			      base   => "ou=users,dc=acu,dc=epita,dc=fr",
			      filter => "uid=$login",
			      attrs => ['objectClass', 'userPassword', 'loginShell'],
			      scope => "sub"
	);
    if ($mesg->code != 0) {
	log(ERROR, $mesg->error);
    }
    if ($mesg->count != 1) {
	log(ERROR, "User $login not found or multiple presence");
    }

    if (grep { "epitaAccount" } $mesg->entry(0)->get_value("objectClass")) {
	log(INFO, "Invalidating password for $login ...");

	my $passwd = $mesg->entry(0)->get_value("userPassword");
	$passwd =~ s/^(\{[^\}]+\})/$1!/ if ($passwd !~ /^\{[^\}]+\}!/);

	$mesg->entry(0)->replace("userPassword" => $passwd);
	$mesg->entry(0)->update($ldap);
    }

    $ldap->unbind or die ("couldn't disconnect correctly");

    if (grep { "posixAccount" } $mesg->entry(0)->get_value("objectClass")) {
	log(DEBUG, "Setting shell for $login ...");
	cmd_account_shell($login, "/bin/false");
    }

    log(WARN, "Done. Don't forget to restart nscd on servers and workstations!");
    return 0;
}

sub cmd_account_cn($@)
{
    return cmd_account_vieworchange('cn', 'name', @_);
}

sub cmd_account_create($@)
{
    my $login = shift;

    if ($#_ < 3) {
	log(USAGE, "lpt account <login> create <year> <uid> <prnom> <nom> [nopass|passgen|password]");
	return 1;
    }

    my $group = shift;

    log(DEBUG, "Adding dn: uid=$login,ou=$group,ou=users,dc=acu,dc=epita,dc=fr ...");

    my $ldap = LDAP::ldap_connect();
    my $mesg = $ldap->add( "uid=$login,ou=$group,ou=users,dc=acu,dc=epita,dc=fr",
			   attrs => [
			       objectclass => [ "top", "epitaAccount" ],
			       uidNumber => shift,
			       cn => shift(@_)." ".shift(@_),
			       mail => "$login\@epita.fr",
			       uid => $login,
			   ]
	);

    #$ldap->unbind or die ("couldn't disconnect correctly");

    if ($mesg->code == 0) {
	log(INFO, "Account added: $login");
	my $pass = shift;
	return cmd_account($login, $pass) if ($pass ne "nopass");
	return 0;
    }
    else {
	log(ERROR, "Unable to add: $login: ", RESET, $mesg->error);
    }
}

sub cmd_account_grantintra($@)
{
    my $login = shift;

    my $ldap = LDAP::ldap_connect();

    my $dn = LDAP::search_dn($ldap, "ou=users", "uid=$login");

    LDAP::add_attribute($ldap, $dn, "objectClass", "intraAccount");

    log(INFO, "$login now grants to use the intranet.");

    $ldap->unbind or die ("couldn't disconnect correctly");
}

sub cmd_account_grantlab($@)
{
    my $login = shift;
    my $group = shift;

    if ($group ne "acu" && $group ne "yaka") {
	log(USAGE, "lpt account <login> grantlab <acu|yaka>");
	return 1;
    }

    my $ldap = LDAP::ldap_connect();

    my $dn = LDAP::search_dn($ldap, "ou=users", "uid=$login");

    if (!LDAP::get_attribute($ldap, $dn, "mail")) {
	LDAP::add_attribute($ldap, $dn, "mail", "$login\@epita.fr");
    }

    LDAP::add_attribute($ldap, $dn, "mailAlias", "$login\@$group.epita.fr");
    LDAP::update_attribute($ldap, $dn, "mailAccountActive", "yes");
    LDAP::add_attribute($ldap, $dn, "objectClass", "MailAccount");
    LDAP::add_attribute($ldap, $dn, "objectClass", "labAccount");

    log(INFO, "$login now grants to receive e-mail and connect in laboratory.");

    $ldap->unbind or die ("couldn't disconnect correctly");
}

sub cmd_account_grantmail($)
{
    my $login = shift;

    my $ldap = LDAP::ldap_connect();

    my $dn = LDAP::search_dn($ldap, "ou=users", "uid=$login");

    my $entry = LDAP::get_dn($ldap, $dn, "mailAccountActive", "objectClass");

    my @oc = $entry->get_value("objectClass");
    push @oc, "MailAccount";

    $entry->replace("objectClass" => \@oc);
    $entry->replace("mailAccountActive" => [ "yes" ]);

    my $mesg = $entry->update($ldap) or die $!;
    if ($mesg->code != 0) { log(WARN, $mesg->error); return 0; }
    else { log(INFO, "$login now grants to receive e-mail. Remember to add some aliases!"); }

    $ldap->unbind or die ("couldn't disconnect correctly");
}

sub cmd_account_mail(@)
{
    return cmd_account_vieworchange('mail', 'mail', @_);
}

sub cmd_account_nopass($@)
{
    my $login = shift;

    my $ldap = LDAP::ldap_connect();

    my $mesg = $ldap->search( # search
			      base   => "ou=users,dc=acu,dc=epita,dc=fr",
			      filter => "uid=$login",
			      attrs => ['userPassword'],
			      scope => "sub"
	);
    if ($mesg->code != 0) {
	log(ERROR, $mesg->error);
    }
    if ($mesg->count != 1) {
	log(ERROR, "User $login not found");
    }

    my $pass = $mesg->entry(0)->get_value("userPassword");

    if (! $pass || $pass eq "{crypt}!toto") {
	$mesg = $ldap->unbind;
	log(WARN, "Password already empty");
	return 2;
    }
    else {
	printf(STDERR "Are you sure you want to reset password for $login? [y/N] ");
	if (getc(STDIN) ne "y") {
	    log(DEBUG, "y response expected to continue; leaving.");
	    log(WARN, "Password unchanged for $login.");
	    return 2;
	}

	$mesg = $ldap->search( # search
			       base   => "ou=users,dc=acu,dc=epita,dc=fr",
			       filter => "uid=$login",
			       attrs => ['userPassword'],
			       scope => "sub"
	    );
	if ($mesg->code != 0) {
	    log(ERROR, $mesg->error);
	}
	if ($mesg->count != 1) {
	    log(ERROR, "User $login not found");
	}

	$mesg->entry(0)->replace("userPassword" => "{crypt}!toto");
	$mesg->entry(0)->update($ldap);

	log(INFO, "$login have no more password.");

	$ldap->unbind or die ("couldn't disconnect correctly");

	return 0;
    }
}

sub cmd_account_passgen($@)
{
    my $login = shift;
    my $nb_char = shift // 10;

    if ($nb_char < 10) {
	log(USAGE, "lpt account <login> passgen [nb_char>=10]");
	return 1;
    }

#printf(STDERR "Are you sure you want to change password for $login? [y/N] ");
#    my $go = <STDIN>;
#    chomp $go;
#    if ($go ne "y" and $go ne "yes") {
#	log(DEBUG, "y response expected to continue, leaving.");
#	log(WARN, "Password unchanged for $login.");
#	return 2;
#    }
#

    log(DEBUG, "Generating a $nb_char chars password...");
    my $pass = "";
    open (HANDLE, "pwgen -s -n -c -y -1 $nb_char 1 |");
    while(<HANDLE>) {
	$pass = $_;
    }
    close(HANDLE);
    chomp($pass);

    log(DEBUG, "Setting $pass password to $login...");
    if (cmd_account_password($login, $pass)) {
	return 3;
    }
    else {
	say "$login:$pass";
	return 0;
    }
}

sub cmd_account_password($@)
{
    my $login = shift;

    if ($#_ > 0) {
	log(USAGE, "lpt account <login> password [new_password]");
	return 1;
    }
    my $pass = shift;

    if (! $pass) {
	say "Changing password for $login.";

	ReadMode("noecho");
	print "new password: "; my $pass1 = <STDIN>;
	print "\nretype new password: "; my $pass2 = <STDIN>;
	ReadMode("restore");
	print "\n";

	log(DEBUG, "Read passwords: $pass1 and $pass2");

	$pass1 eq $pass2 || log(ERROR, "Passwords did not match.");
	$pass = $pass1;
    }

    if ($pass eq "") {
	log(ERROR, "Empty password refused.");
    }

    chomp($pass);
    my $salt = join '', ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[rand 64, rand 64, rand 64, rand 64];

    my $ctx = Digest::SHA1->new;
    $ctx->add($pass);
    $ctx->add($salt);

    my $enc_password = "{SSHA}" . encode_base64($ctx->digest . $salt ,'');

    my $ldap = LDAP::ldap_connect();

    my $mesg = $ldap->search( # search
			      base   => "ou=users,dc=acu,dc=epita,dc=fr",
			      filter => "uid=$login",
			      attrs => ['userPassword'],
			      scope => "sub"
	);
    if ($mesg->code != 0) {
	log(ERROR, $mesg->error);
    }
    if ($mesg->count != 1) {
	log(ERROR, "User $login not found");
    }

    $mesg->entry(0)->replace("userPassword" => $enc_password);
    $mesg->entry(0)->update($ldap);
    $ldap->unbind or die ("couldn't disconnect correctly");
    return 0;
}

sub cmd_account_photo($@)
{
    return cmd_account_vieworchange('photoURI', 'photo', @_);
}

sub cmd_account_reopen(@)
{
    my $login = shift;

    if ($#_ != -1) {
	log(USAGE, "<lpt> account <login> reopen");
	return 1;
    }

    my $ldap = LDAP::ldap_connect();

    my $mesg = $ldap->search( # search
			      base   => "ou=users,dc=acu,dc=epita,dc=fr",
			      filter => "uid=$login",
			      attrs => ['objectClass', 'cn', 'userPassword', 'loginShell'],
			      scope => "sub"
	);
    if ($mesg->code != 0) {
	log(ERROR, $mesg->error);
    }
    if ($mesg->count != 1) {
	log(ERROR, "User $login not found or multiple presence");
    }

    if (grep { "epitaAccount" } $mesg->entry(0)->get_value("objectClass")) {
	# update password
	my $passwd = $mesg->entry(0)->get_value("userPassword");
	if ($passwd =~ /^\{[^\}]+\}!/) {
	    log(INFO, "Restoring password for $login ...");

	    $passwd =~ s/^(\{[^\}]+\})!/$1/;

	    $mesg->entry(0)->replace("userPassword" => $passwd);
	    $mesg->entry(0)->update($ldap);
	}
    }

    $ldap->unbind or die ("couldn't disconnect correctly");

    if (grep { "posixAccount" } $mesg->entry(0)->get_value("objectClass")) {
	log(DEBUG, "Setting shell for $login ...");
	cmd_account_shell($login, $shellValid);
    }

    log(WARN, "Done. Don't forget to restart nscd on servers and workstations!");
    return 0;
}

sub cmd_account_rights($@)
{
    return cmd_account_multiple_vieworchange("intraRight", "right", @_);
}

sub cmd_account_services($@)
{
    return cmd_account_multiple_vieworchange("labService", "laboratory_service", @_);
}

sub cmd_account_shell($@)
{
    return cmd_account_vieworchange("loginShell", "shell", @_);
}

sub cmd_account_multiple_vieworchange($$$@)
{
    my $type = shift;
    my $typeName = shift;
    my $login = shift;
    my $action = shift // "list";
    my $change = shift;

    if (($action ne "list" and $action ne "add" and $action ne "del" and $action ne "flush") or (!$change and $action ne "list" and $action ne "flush")) {
	log(USAGE, "<lpt> account <login> $typeName [list|add|del|flush] [string]");
	return 1;
    }

    my $ldap;
    $ldap = LDAP::ldap_connect() if ($action ne "list");
    $ldap = LDAP::ldap_connect_anon() if ($action eq "list");
    my $mesg = $ldap->search( # search
			      base   => "ou=users,dc=acu,dc=epita,dc=fr",
			      filter => "uid=$login",
			      attrs => [ $type ],
			      scope => "sub"
	);
    if ($mesg->code != 0) {
	log(ERROR, $mesg->error);
    }
    if ($mesg->count != 1) {
	log(ERROR, "User $login not found or multiple presence");
    }

    if ($action eq "add") {
	log(INFO, "Adding $change as ".$typeName."s for $login ...");

	my @data = $mesg->entry(0)->get_value($type);

	if (! grep(/^$change$/, @data)) {
	    push @data, $change;
	    $mesg->entry(0)->replace($type => \@data) or die $!;
	    $mesg->entry(0)->update($ldap) or die $!;

	    log(INFO, "Done!");
	}
	else {
	    log(WARN, "$login has already $change $typeName.");
	}
    }
    elsif ($action eq "del") {
	log(INFO, "Checking if $change is a ".$typeName."s of $login ...");
	my @data = $mesg->entry(0)->get_value($type);
	if (grep(/^$change$/, @data)) {
	    log(INFO, "Deleting $change as $typeName for $login ...");

	    @data = grep(!/$change$/, @data);

	    $mesg->entry(0)->replace($type => \@data) or die $!;
	    $mesg->entry(0)->update($ldap) or die $!;

	    log(INFO, "Done!");
	}
	else {
	    log(WARN, "$change is not a $typeName for $login.");
	}
    }
    elsif ($action eq "flush") {
	$ldap->modify($mesg->entry(0)->dn, delete => [$type]);
	log(INFO, "$login have no more $typeName.");
    }
    else {
	if ($mesg->entry(0)->get_value($type)) {
	    log(INFO, $login."'s ".$typeName."s are:");
	    for my $val ($mesg->entry(0)->get_value($type)) {
		say "  -  $val";
	    }
	}
	else {
	    log(INFO, "$login have no $typeName.");
	}
    }

    $ldap->unbind or die ("couldn't disconnect correctly");
    return 0;
}

sub cmd_account_vieworchange($$@)
{
    my $type = shift;
    my $typeName = shift;
    my $login = shift;

    if ($#_ > 0) {
	log(USAGE, "<lpt> account <login> $typeName [new_string]");
	return 1;
    }

    my $change = shift;

    my $ldap;
    $ldap = LDAP::ldap_connect() if ($change);
    $ldap = LDAP::ldap_connect_anon() if (!$change);

    my $mesg = $ldap->search( # search
			      base   => "ou=users,dc=acu,dc=epita,dc=fr",
			      filter => "uid=$login",
			      attrs => [ $type ],
			      scope => "sub"
	);
    if ($mesg->code != 0) {
	log(ERROR, $mesg->error);
    }
    if ($mesg->count != 1) {
	log(ERROR, "User $login not found or multiple presence");
    }

    if ($change) {
	log(INFO, "Setting $typeName to $change for $login ...");

	$mesg->entry(0)->replace($type => $change) or die $!;
	$mesg->entry(0)->update($ldap) or die $!;

	log(INFO, "Done!");
    }
    elsif ($mesg->entry(0)->get_value($type)) {
	log(INFO, $login."'s $typeName is ".$mesg->entry(0)->get_value($type).".");
    }
    else {
	log(INFO, $login."'s has no $typeName.");
    }

    $ldap->unbind or die ("couldn't disconnect correctly");
    return 0;
}

sub cmd_account_view($@)
{
    my $login = shift;

    my $ldap = LDAP::ldap_connect_anon();

    my $mesg = $ldap->search(base   => "ou=users,dc=acu,dc=epita,dc=fr",
			     filter => "uid=$login",
			     attrs  => ['objectClass']);

    $mesg->code && log(ERROR, $mesg->error);
    if ($mesg->count <= 0) {
	log(ERROR, "No such account!");
    }

    log(DEBUG, "objectClasses:\t" . join(', ', $mesg->entry(0)->get_value("objectClass")));

    my @attrs = ['dn', 'ou'];
    if ($#_ >= 0) {
	push @attrs, @_;
    }
    else {
	if (grep { "epitaAccount" } $mesg->entry(0)->get_value("objectClass")) {
	    push @attrs, 'uid', 'cn', 'mail', 'uidNumber';
	}
	if (grep { "posixAccount" } $mesg->entry(0)->get_value("objectClass")) {
	    push @attrs, 'gecos', 'loginShell', 'homeDirectory', 'gidNumber';
	}
	if (grep { "labAccount" } $mesg->entry(0)->get_value("objectClass")) {
	    push @attrs, 'labService', 'quotaHomeBlock', 'quotaHomeFile', 'quotaSgoinfreBlock', 'quotaSgoinfreFile';
	}
	if (grep { "intraAccount" } $mesg->entry(0)->get_value("objectClass")) {
	    push @attrs, 'intraRight';
	}
	if (grep { "MailAccount" } $mesg->entry(0)->get_value("objectClass")) {
	    push @attrs, 'mailAlias';
	}
    }

    log(DEBUG, "attrs to get: " . join(', ', @attrs));
    $mesg = $ldap->search(base   => "ou=users,dc=acu,dc=epita,dc=fr",
			  filter => "uid=$login",
			  attrs  => \@attrs);
    $mesg->code && die $mesg->error;

    shift @attrs; # Remove dn
    my $nb = 0;
    for my $entry ($mesg->entries)
    {
	if ($nb > 0) {
	    say "==";
	}
	say BOLD, YELLOW, "dn: ", RESET, YELLOW, $entry->dn, RESET;

	for my $attr (@attrs) {
	    if ($#attrs < 3) {
		for my $entry ($entry->get_value($attr)) {
		    say CYAN, "$attr: ", RESET , $entry;
		}
	    }
	    else {
		say CYAN, "$attr: ", RESET , join(', ', $entry->get_value($attr));
	    }
	}

	$nb++;
    }

    if ($nb > 1) {
	say "\n$nb users displayed";
    }

    $ldap->unbind or die ("couldn't disconnect correctly");
    return 0;
}


######################################
#                                    #
#   GROUP BLOCKS		     #
#                                    #
######################################

sub cmd_group(@)
{
    my $gname = shift;

    if (! $gname) {
	log(USAGE, "lpt group <group-name> <command> [arguments ...]");
	return 1;
    }

    my $subcmd = shift // "view";

    if (! $subcmd) {
	pod2usage(-verbose => 99,
		  -sections => [ 'GROUP COMMANDS' ] );
    }
    elsif (! exists $cmds_group{$subcmd}) {
	log(USAGE, "Unknown command for group: ". $subcmd);
	return 1;
    }

    return $cmds_group{$subcmd}($gname, @_);
}

sub cmd_group_list(@)
{
    if ($#ARGV > 0)
    {
	log(USAGE, "<lpt> group list [group]");
	exit(1);
    }

    my $group = $ARGV[0];
    my $ldap = LDAP::ldap_connect_anon();
    if ($#ARGV == 0)
    {
	my $mesg = $ldap->search( # search a group
				  base   => "cn=$group,ou=groups,dc=acu,dc=epita,dc=fr",
				  filter => "objectClass=posixGroup",
				  attrs => ['memberUid']
	    );
	$mesg->code && die $mesg->error;
	$mesg->count > 0 || return -1;

	foreach my $entry ($mesg->sorted('memberUid'))
	{
	    foreach my $user ($entry->get_value("memberUid"))
	    {
		print "$user\n";
	    }
	}
    }
    else
    {
	my $mesg = $ldap->search( # list groups
				  base   => "ou=groups,dc=acu,dc=epita,dc=fr",
				  filter => "objectClass=posixGroup",
				  attrs => ['cn', 'gidNumber']
	    );


    	$mesg->code && die $mesg->error;
    	$mesg->count > 0 || return -1;

    	foreach my $entry ($mesg->sorted('gidNumber'))
	{
	    print $entry->get_value("cn")." --->";
	    print $entry->get_value("gidNumber")."\n";
	}
    }

    $ldap->unbind;   # take down session
}

sub cmd_group_add(@)
{
    my $group = shift;

    if ($#_ < 0)
    {
	log(USAGE, "<lpt> group <group-name> add <login>");
    	exit(1);
    }

    my $login = shift;

    my $ldap = LDAP::ldap_connect();

    my $mesg = $ldap->search( # search a group
			      base   => "cn=$group,ou=system,ou=groups,dc=acu,dc=epita,dc=fr",
			      filter => "objectClass=posixGroup",
			      attrs => ['memberUid']
	) or die $!;
    $mesg->code && die $mesg->error;
    $mesg->count > 0 || return -1;

    foreach my $entry ($mesg->entries)
    {
	my @mem = $entry->get_value("memberUid");

	foreach my $member (@mem)
        {
	    if ($member eq $login)
	    {
		log WARN, "$login est dj dans le groupe $group";
		$ldap->unbind;
		exit 1;
	    }
	}

	push @mem, $login;
	$entry->replace("memberUid" => \@mem);
        $entry->update($ldap);

	log INFO, "$login ajout au groupe $group avec succs.";
    }
    $ldap->unbind;   # take down session
}

sub cmd_group_remove(@)
{
    if ($#ARGV < 1)
    {
        log(USAGE, "<lpt> group remove <group> <login>");
        exit(1);
    }

    my $group = $ARGV[0];
    my $login = $ARGV[1];

    my $ldap = LDAP::ldap_connect();

    my $mesg = $ldap->search( # search a group
			      base   => "cn=$group,ou=groups,dc=acu,dc=epita,dc=fr",
			      filter => "objectClass=posixGroup",
			      attrs => ['memberUid']
	);
    $mesg->code && die $mesg->error;
    $mesg->count > 0 || return -1;

    foreach my $entry ($mesg->sorted('memberUid'))
    {
        my @mem = $entry->get_value("memberUid");
	my $found = 0;
        foreach my $user (@mem)
        {
            if ($user eq $login)
	    {
		$found = 1;
	    }
        }

	if ($found)
    	{
	    @mem = grep(!/$login$/, @mem);
	    $entry->replace("memberUid" => [@mem]);
    	    $entry->update($ldap);
    	}
    	else
    	{
    	    print "$login n'est pas dans le groupe $group\n";
    	}

        print "Nouvelle liste des membres de $group :\n";
        foreach my $user (@mem)
        {
            print "$user\n";
        }

    }
    $ldap->unbind;   # take down session

    system('service nscd restart');
}

sub cmd_group_create($$)
{
    if ($#_ != 1)
    {
	log(USAGE, "<lpt> group create <yaka|acu> <year>");
	exit(1);
    }

    my $type = shift;
    my $year = shift;
    my $cn = $type . $year;
    my $gid;
    if ($type eq "acu") {
	$gid = $year;
    }
    elsif ($type eq "yaka") {
	$gid = $year - 1000;
    }
    else {
	log(ERROR, "Error: type must be acu or yaka!");
    }

    my $ldap = LDAP::ldap_connect();

    my $mesg = $ldap->add( "cn=$cn,ou=groups,dc=acu,dc=epita,dc=fr",
		attrs => [
		    objectclass => "posixGroup",
		    gidNumber => $gid,
		    cn => $cn,
		]
	);
    if ($mesg->code != 0) { die $mesg->error; }

    $ldap->unbind or die ("couldn't disconnect correctly");

    log(INFO, "group added: $cn");
}

sub cmd_group_delete(@)
{
    if ($#ARGV != 1)
    {
	log(USAGE, "<lpt> group delete <yaka|acu> <year>");
	exit(1);
    }

    print "TODO!";
    print "hint: ldapdelete -v -h ldap.acu.epita.fr -x -w \$LDAP_PASSWD -D 'cn=admin,dc=acu,dc=epita,dc=fr' 'cn=yaka2042,ou=groups,dc=acu,dc=epita,dc=fr'";
    exit(1);
}


######################################
#				     #
# LIST BLOCK			     #
#				     #
######################################

sub cmd_list(@)
{
    my $subcmd = shift;

    if (! $subcmd) {
	pod2usage(-verbose => 99,
		  -sections => [ 'LIST COMMANDS' ] );
    }
    elsif (! exists $cmds_list{$subcmd}) {
	log(USAGE, "Unknown command for list: ". $subcmd);
	return 1;
    }

    return $cmds_list{$subcmd}(@_);
}

sub cmd_list_accounts(@)
{
    if ($#_ > 1)
    {
	log(USAGE, "<lpt> list account [open|close|services]");
	exit(1);
    }
    my $action = shift // "open";

    my $shellFalse = "/bin/false";
    my $ldap = LDAP::ldap_connect();

    if ($action eq "open")
    {
	my $mesg = $ldap->search(base   => "ou=users,dc=acu,dc=epita,dc=fr",
				 filter => "&(!(loginShell=$shellFalse))(|(objectClass=posixAccount)(objectClass=epitaAccount))",
				 attrs  => [ 'dn', 'userPassword' ]);
	$mesg->code && die $mesg->error;
	if ($mesg->count == 0) {
	    log(WARN, "No account found");
	}
	else {
	    for my $entry ($mesg->entries) {
		if (! $entry->get_value("userPassword") or $entry->get_value("userPassword") =~ /^\{[^\}]\}!/) {
		    print YELLOW, "Partially closed:\t", RESET;
		} else {
		    print CYAN, "Opened:\t", RESET;
		}
		say $entry->dn;
	    }
	}
    }
    elsif ($action eq "close")
    {
	my $mesg = $ldap->search(base   => "ou=users,dc=acu,dc=epita,dc=fr",
				 filter => "&(loginShell=$shellFalse)(|(objectClass=posixAccount)(objectClass=epitaAccount))",
				 attrs  => [ 'userPassword' ]);
	$mesg->code && die $mesg->error;
	if ($mesg->count == 0) {
	    log(WARN, "No account found");
	}
	else {
	    for my $entry ($mesg->entries) {
		if ($entry->get_value("userPassword") =~ /^\{[^\}]\}!/) {
		    print YELLOW, "Partially closed:\t", RESET;
		} else {
		    print RED, "Closed:\t", RESET;
		}
		say $entry->dn;
	    }
	}
    }
    elsif ($action eq "services")
    {
	my $service = shift // "*";

	my $mesg = $ldap->search(base   => "ou=users,dc=acu,dc=epita,dc=fr",
				 filter => "&(labService=$service)(|(objectClass=posixAccount)(objectClass=epitaAccount))",
				 attrs  => [ 'uid', 'labService' ]);
	$mesg->code && die $mesg->error;
	if ($mesg->count == 0) {
	    log(WARN, "No account found!");
	}
	else {
	    for my $entry ($mesg->entries) {
		say YELLOW, $entry->get_value("uid"), "\t", RESET, join(", ", $entry->get_value("labService"));
	    }
	}
    }

    $ldap->unbind or die ("couldn't disconnect correctly");
    return 0;
}


######################################
#                                    #
#   QUOTA COMMAND		     #
#                                    #
######################################

sub cmd_account_quota($@)
{
    my $login = shift;

    my $action = shift;

    if ($#_ >= 0) {
	cmd_account_quota_set($login, $action, @_);
    }
    elsif ($action eq "sync") {
	cmd_account_quota_sync($login, @_);
    }
    else {
	cmd_account_quota_view($login, @_);
    }
}

sub cmd_account_quota_view($@)
{
    my $login = shift;

    my $ldap = LDAP::ldap_connect_anon();
    my $mesg = $ldap->search(
			   base   => "ou=users,dc=acu,dc=epita,dc=fr",
			   filter => "uid=$login",
			   attrs  => [ 'quotaHomeBlock', 'quotaHomeFile', 'quotaSgoinfreBlock', 'quotaSgoinfreFile' ]
			   );

    $mesg->code && die $mesg->error;
    $mesg->count > 0 || return -1;

    my $nb = 0;
    foreach my $entry ($mesg->entries)
    {
	if ($nb > 0) {
	    say "==";
	}
	say BOLD, YELLOW, "dn: ", RESET, YELLOW, $entry->dn, ":", RESET;
	say " - ", BLUE, "Home blocks:\t\t", RESET, ($entry->get_value("quotaHomeBlock") or "(standard)");
	say " - ", BLUE, "Home files:\t\t", RESET, ($entry->get_value("quotaHomeFile") or "(standard)");
	say " - ", BLUE, "Sgoinfre blocks:\t", RESET, ($entry->get_value("quotaSgoinfreBlock") or "(standard)");
	say " - ", BLUE, "Sgoinfre files:\t", RESET, ($entry->get_value("quotaSgoinfreFile") or "(standard)");

	$nb++;
    }

    $ldap->unbind or die ("couldn't disconnect correctly");
}

sub cmd_account_quota_set($@)
{
    my $login = shift;

    if ($#_ > 2)
    {
	log(USAGE, "<lpt> account <login> quota <volume> <type> <value>");
	return 1;
    }

    my $volume = shift;
    my $type   = shift;
    my $value  = shift;

    # check args
    if (!($volume eq "home" || $volume eq "sgoinfre")) {
	log(ERROR, "Volume must be home or sgoinfre; given: $volume");
    }
    if (!($type eq "file" || $type eq "block")) {
	log(ERROR, "Type must be file or block; given: $type");
    }

    # generate quotaName
    my $quotaName = "quota";
    $quotaName .= "Home" if ($volume eq "home");
    $quotaName .= "Sgoinfre" if ($volume eq "sgoinfre");
    $quotaName .= "File" if ($type eq "file");
    $quotaName .= "Block" if ($type eq "block");

    my $ldap;
    $ldap = LDAP::ldap_connect() if ($value);
    $ldap = LDAP::ldap_connect_anon() if (!$value);
    my $mesg = $ldap->search( # search
			      base   => "ou=users,dc=acu,dc=epita,dc=fr",
			      filter => "uid=$login",
			      attrs => [ $quotaName ],
			      scope => "sub"
	);
    if ($mesg->code != 0) { log(ERROR, $mesg->error); }
    if ($mesg->count != 1) { log(ERROR, "user $login not found or multiple presence"); }

    my $old_value = $mesg->entry(0)->get_value($quotaName);
    if (!$old_value) {
	$old_value = $def_quota{$type}{$volume};
    }

    if (!$value) {
	say YELLOW, "dn: ", $mesg->entry(0)->dn, RESET;
	say BLUE, $quotaName, ": ", RESET, $old_value;
	return 0;
    }

    if ($value =~ '^\+([0-9]+)([MKGTmkgt]?)$') {
	my $t = $1;
	$t *= 1024 if ($2 eq "K" or $2 eq "k");
	$t *= 1048576 if ($2 eq "M" or $2 eq "m");
	$t *= 1073741824 if ($2 eq "G" or $2 eq "g");
	$t *= 1099511627776 if ($2 eq "T" or $2 eq "t");
	$value = $old_value + $t;
    }
    elsif ($value =~ '^-([0-9]+)([MKGTmkgt]?)$') {
	my $t = $1;
	$t *= 1024 if ($2 eq "K" or $2 eq "k");
	$t *= 1048576 if ($2 eq "M" or $2 eq "m");
	$t *= 1073741824 if ($2 eq "G" or $2 eq "g");
	$t *= 1099511627776 if ($2 eq "T" or $2 eq "t");
	$value = $old_value - $t;
    }
    elsif ($value !~ /^[0-9]+$/) {
	log(ERROR, "Value must be an integer or +i or -i");
    }

    log(INFO, "Changing quota of $quotaName of $login to $value...");

    $mesg->entry(0)->replace($quotaName => $value) or die $!;
    $mesg->entry(0)->update($ldap) or die $!;

    $ldap->unbind;

    log(INFO, "Done!");
}

sub cmd_account_quota_sync($;$)
{
    my $login = shift;
    my $nosync = shift;

    my $ldap = LDAP::ldap_connect_anon();
    my $mesg = $ldap->search(
			   base   => "ou=users,dc=acu,dc=epita,dc=fr",
			   filter => "(&(uid=$login)(objectClass=labAccount))",
			   attrs  => [ 'uid', 'uidNumber',
				       'quotaHomeBlock', 'quotaHomeFile',
				       'quotaSgoinfreBlock', 'quotaSgoinfreFile' ]
			   );
    $mesg->code && die $mesg->error;
    $mesg->count == 1 || log(ERROR, "User $login not found or multiple presence");

    my $quotaHomeBlock = $mesg->entry(0)->get_value("quotaHomeBlock") // $def_quota{block}{home};
    my $quotaHomeFile = $mesg->entry(0)->get_value("quotaHomeFile") // $def_quota{file}{home};
    my $quotaSgoinfreBlock = $mesg->entry(0)->get_value("quotaSgoinfreBlock") // $def_quota{block}{sgoinfre};
    my $quotaSgoinfreFile = $mesg->entry(0)->get_value("quotaSgoinfreFile") // $def_quota{file}{sgoinfre};

    if (Quota::setqlim($dev_quota{home}, $mesg->entry(0)->get_value("uidNumber"), int(0.9 * $quotaHomeBlock), $quotaHomeBlock, int(0.9 * $quotaHomeFile), $quotaHomeFile, 1, 0) == 0 and
	Quota::setqlim($dev_quota{sgoinfre}, $mesg->entry(0)->get_value("uidNumber"), int(0.9 * $quotaHomeBlock), $quotaHomeBlock, int(0.9 * $quotaHomeFile), $quotaHomeFile, 1, 0) == 0) {
	log(INFO, $login."'s quota synchronized!");
    }
    else {
	log(ERROR, "An error occurs during quota synchronization:");
	Quota::strerr();
	return 2;
    }

    $ldap->unbind or die ("couldn't disconnect correctly");

    if (!$nosync) {
	Quota::sync($dev_quota{home});
	Quota::sync($dev_quota{sgoinfre});
    }

    return 0;
}

sub cmd_sync_quota(@)
{
    my $ldap = LDAP::ldap_connect_anon();
    my $mesg = $ldap->search(
			   base   => "ou=users,dc=acu,dc=epita,dc=fr",
			   filter => "(objectClass=labAccount)",
			   attrs  => [ 'uid' ]
			   );
    $mesg->code && die $mesg->error;

    $ldap->unbind or die ("couldn't disconnect correctly");

    for my $entry ($mesg->entries) {
	cmd_account_quota_sync($entry->get_value("uid"), 1);
    }
}


######################################
#                                    #
#   QUOTA COMMAND		     #
#                                    #
######################################

sub get_ssh_keys_unprotected()
{
    my %keys_unprotected = qw();

    my $ldap = LDAP::ldap_connect_anon();
    my $mesg = $ldap->search(
			   base   => "ou=users,dc=acu,dc=epita,dc=fr",
			   filter => "(objectClass=posixAccount)",
			   attrs  => ['uid','cn', 'homeDirectory']
			   );

    $mesg->code && die $mesg->error;
    $mesg->count > 0 || return -1;

    foreach my $entry ($mesg->sorted('uid'))
    {
	my $home = $entry->get_value("homeDirectory");
	$home =~ s#^$wksHomePrefix#$nfsHomePrefix#;
	my $sshDir = $home . "/.ssh";
	my $login = $entry->get_value("uid");

	if (-d $sshDir)
	{
	    my $process_file = sub() {
		my $file = $_;
		if (-f $file) {
		    open my $fh, '<', $file or die $!;
		    my @lines = <$fh>;
		    close $fh;
		    if ( grep { chomp; $_ =~ /PRIVATE KEY/ } @lines )
		    {
			if (! grep { chomp; $_ =~ /ENCRYPTED/ } @lines )
			{
			    if (!exists $keys_unprotected{$login})
			    {
				$keys_unprotected{$login} = [$file];
			    }
			    else
			    {
				push(@{$keys_unprotected{$login}}, $file);
			    }
			}
		    }
		}
	    };

	    find({ wanted => \&$process_file, no_chdir => 1 }, $sshDir);
	}
    }

    $ldap->unbind or die ("couldn't disconnect correctly");

    return %keys_unprotected;
}

sub cmd_ssh_keys_without_passphrase_generic(@)
{
    my $func = shift;

    my %keys_unprotected = get_ssh_keys_unprotected();
    my $ldap = LDAP::ldap_connect_anon();

    foreach my $login (keys %keys_unprotected)
    {
	my $mesg = $ldap->search(
	    base   => "ou=users,dc=acu,dc=epita,dc=fr",
	    filter => "uid=$login",
	    attrs  => [ 'uid', 'cn', 'mailAlias' ]
	    );

	$mesg->code && die $mesg->error;
	$mesg->count > 0 || return -1;

	my $entry = $mesg->entry(0);

	# Apply func
	&$func($entry, \@{$keys_unprotected{$login}});
    }

    $ldap->unbind or die ("couldn't disconnect correctly");
}

# list unprotected keys
sub cmd_ssh_keys_without_passphrase_show(@)
{
    my $process = sub() {
	my $entry = shift;
	my $keys = shift;

	# Display
	print $entry->get_value("cn").":\n";
	foreach my $key (@$keys)
	{
	    print " * $key\n";
	}
	print "\n";
    };

    cmd_ssh_keys_without_passphrase_generic(\&$process);
}

# warn about unprotected keys
sub cmd_ssh_keys_without_passphrase_warn(@)
{
    my $process = sub() {
	my $entry = shift;
	my $keys = shift;

	# Display
	print $entry->get_value("uid")."\n";

	# create the message
	#use Mail::Internet;

	my $body = "Bonjour ".$entry->get_value("cn").",

Un outil automatique a dcouvert une cl sans passphrase sur votre compte
du laboratoire. Il est impratif de mettre une passphrase chiffrant votre
cl pour des raisons de scurit.

Les clefs non protges sont les suivantes :\n";
	foreach my $key (@$keys)
	{
	    $key =~ s#^$nfsHomePrefix#$wksHomePrefix#;
	    $body .= " - $key\n";
	}
	$body .= "\nPour mettre une passphrase :
\$ ssh-keygen -p -f CHEMIN_VERS_LA_CLE_PRIVEE

Merci de rectifier la situation au plus vite ou votre cl sera supprime et
votre compte sera mis en suspens.

Cordialement,

PS: Ce message est gnr automatiquement, les roots sont en copie.
    Pour toute demande, merci de faire un ticket  admin\@acu.epita.fr

--
Les roots ACU";

	#my $email = Mail::Internet->new();
	#$email->body($body);
	#$email->add( "To", $entry->get_value("mailAlias") );
	#$email->add( "Cc", "<root\@acu.epita.fr>" );
	#$email->add( "From", "Roots assistants <admin\@acu.epita.fr>" );
	#$email->add( "Subject", "[LAB][SSH-PASSPHRASE] Clef SSH non protge" );
	#$email->send();
    };

    cmd_ssh_keys_without_passphrase_generic(\&$process);
}

# remove unprotected keys
sub cmd_ssh_keys_without_passphrase_remove(@)
{
    my $process = sub() {
	my $entry = shift;
	my $keys = shift;

	# Display
	print $entry->get_value("uid")."\n";

	# create the message
	use Email::MIME;
	my $body = "Bonjour ".$entry->get_value("cn").",

Un outil automatique a dcouvert une clef sans passphrase sur votre
compte du laboratoire.

N'ayant pas corrig votre situation aprs plusieurs relances, nous avons
dsactiv votre compte et supprim le(s) clef(s) incrimines.

Pour information, voici l'empreinte de chacune des clefs supprime :\n";
	foreach my $key (@$keys)
	{
	    open (FNGR, "ssh-keygen -l -f '$key' | cut -d ' ' -f 2");
	    my $fingerprint = <FNGR>;
	    chomp $fingerprint;
	    close (FNGR);

	    unlink($key);

	    $key =~ s#^$nfsHomePrefix#$wksHomePrefix#;
	    $body .= " * $key: $fingerprint\n";
	}
	$body .= "\n
Contacter les roots pour faire reouvrir votre compte.

Cordialement,

PS: Ce message est gnr automatiquement, les roots sont en copie.
    Pour toute demande, merci de faire un ticket  admin\@acu.epita.fr

--
Les roots ACU";

	#my $email = Mail::Internet->new();
	#$email->body($body);
	#$email->add( "To", $entry->get_value("mailAlias") );
	#$email->add( "Cc", "<root\@acu.epita.fr>" );
	#$email->add( "From", "Roots assistants <admin\@acu.epita.fr>" );
	#$email->add( "Subject", "[LAB][SSH-PASSPHRASE] Clef SSH non protge supprime" );
	#$email->send();
    };

    cmd_ssh_keys_without_passphrase_generic(\&$process);
}


######################################
#                                    #
#   MAIN CORE			     #
#                                    #
######################################

sub cmd_help
{
    pod2usage(-exitval => 1, -verbose => 2);
}

if ($#ARGV == -1) {
    cmd_help();
    exit(1);
}

my $cmd = shift;

if ($cmd eq "-v" or $cmd eq "--verbose" or $cmd eq "--debug") {
    $ACU::Log::display_level = 8;
    $cmd = shift;
}
elsif ($cmd eq "-q" or $cmd eq "--quiet") {
    $ACU::Log::display_level = 6;
    $cmd = shift;
}

$ACU::Log::fatal_error = 1;
$ACU::Log::fatal_warn = 0;

if (! exists $cmds{$cmd})
{
    say BOLD, "Usage: ", RESET, "$0 ", GREEN, "command", RESET, " <arguments>";
    log(ERROR, "Uknown command : $cmd");
}

exit ($cmds{$cmd}(@ARGV));

__END__
=head1 NAME

lpt - Lab Power Tool

=head1 SYNOPSIS

B<lpt> I<command> [arguments]

I<command> can be:

B<lpt> I<account> <login> [arguments]

	Manage the account <login>.

B<lpt> I<group> <group-name> [arguments]

	Manage the group <group-name>

B<lpt> I<help>

	Display this screen.

B<lpt> I<year> [year]

	Set or display the current year.


=head1 ACCOUNT COMMANDS

B<lpt account> <login> [I<view>]

	Display information about <login>.

	<login> can be a globbing string.

B<lpt account> <login> I<create> <promo> <uid> <Prenom> <Nom> [nopass|password|passgen]

	This is used to create a new Epita account, base for intra and/or lab account.

	Promo for professor are professors, other people are guests.

B<lpt account> <login> I<nopass>

	This is used to erase the userPassword.

B<lpt account> <login> I<close>

	This is used to close an existing account.

B<lpt account> <login> I<reopen>

	This is used to reopen a previously closed account.

B<lpt account> <login> I<shell> <shell path>

	This is used to change default shell for an existing accout.

B<lpt account> <login> I<passgen> [nb_char]

	This is used to set user password. Generated by pwgen.

	nb_char must be at least egal to 10.

B<lpt account> <login> I<password> [password]

	This is used to set user password. Interactively asked if not given.

B<lpt account> I<mail> <login> [new]

	This is used to get user email (to which are forwarded his emails) if
	'new' is empty, and to change it if 'new' is given.

B<lpt account> I<list> <open | close | service>

	List accounts: with access to the PILA, without, with access to
        services.

B<lpt account> I<finger> <login>

	Display information about a login.

B<lpt account> I<service_flush> <login>

	Remove all services associated to a login.


=head1 GROUP COMMANDS

B<lpt group> I<list> [group]

	This is used to list groups available on the PIL or to list the members
	of the specified group.

B<lpt group> I<add> <group> <login>

	This is used to add a user to a posix group.

B<lpt group> I<create> <yaka | acu> <year>

	This is used to create a posix group.

B<lpt group> I<remove> <group> <login>

	This is used to remove a user from a posix group.

B<lpt group> I<delete> <yaka | acu> <year>

	This is used to delete a posix group.


=head1 QUOTA COMMANDS

B<lpt quota> I<show> <login>

	Display the quota of everyone or someone.

B<lpt quota> I<set> <login> <volume> <type> <value>

	Set the quota of someone. Volume is home/sgoinfre and type is
        block/file.

=head1 SERVICE COMMANDS

B<lpt service> I<add> <login> <name>

	This is used to add a service to a user.

B<lpt service> I<remove> <login> <name>

	This is used to remove a service from a user.


=head1 SSH_KEYS_WITHOUT_PASSPHRASE COMMANDS

B<lpt ssh> I<keys-without-passphrase> <show | warn | remove>

	Search for users with SSH keys without passphrase. Warn the users and
        remove them if requested.


=head1 DESCRIPTION

B<lpt> is a tool developed to replace ancient perl scripts used to manage
accounts, and some other stuff.
The goal was to give an unique tool with meaningful commands to perform
usual operations. lpt is born from ipt.

=head1 AUTHORS

Project started by : Adnan Aita <I<ski@epita.fr>>, root@acu 2006

Modified by Laroche Emeric <I<laroch_e@epita.fr>>, root@acu 2007

Modified by Sterckeman Julien <I<sterck_j@epita.fr>>, root@acu 2008

Modified by Sebastien Luttringer <I<seblu@epita.fr>>, root@acu 2008

Modified by Vincent Nguyen <I<nguyen_v@epita.fr>>, root@acu 2010

Modified by JB et Antoine <I<root@acu.epita.fr>>, root@acu 2012

Modified by megra <I<j@marguerie.org>>, root@acu 2013 : added tons of features :)

Strongly modified by nemunaire & nicolas, root@acu 2014

=head1 VERSION

This is B<lpt> version 1.1.

=head1 TODO

Tons of stuff :
 * delete account
 * group delete
 * ...

=head1 BUGS

No bug, just features.

=cut
