epita-std
/
ACU
Archived
1
0
Fork 0
This repository has been archived on 2021-10-08. You can view files and clone it, but cannot push or open issues or pull requests.
ACU/utils/lpt

1713 lines
41 KiB
Perl
Executable File
Raw Blame History

#!/usr/bin/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 Quota;
#use Cwd 'abs_path';
#use File::Basename;
#use File::Find;
BEGIN {
push @INC, "../";
}
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,
);
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> <pr<70>nom> <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_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) {
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(@)
{
if ($#ARGV < 1)
{
log(USAGE, "<lpt> group add <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']
) or die $!;
$mesg->code && die $mesg->error;
$mesg->count > 0 || return -1;
foreach my $entry ($mesg->sorted('memberUid'))
{
my @mem = $entry->get_value("memberUid");
foreach my $user (@mem)
{
if ($user eq $login)
{
print "$login est deja dans le groupe $group\n";
$ldap->unbind;
exit -1;
}
}
push(@mem, $login);
$entry->replace("memberUid" => [@mem]);
$entry->update($ldap);
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_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 Email::MIME;
my $body = "Bonjour ".$entry->get_value("cn").",
Un outil automatique a d<>couvert une cl<63> sans passphrase sur votre compte
du laboratoire. Il est imp<6D>ratif de mettre une passphrase chiffrant votre
cl<EFBFBD> pour des raisons de s<>curit<69>.
Les clefs non prot<6F>g<EFBFBD>es 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<63> sera supprim<69>e et
votre compte sera mis en suspens.
Cordialement,
PS: Ce message est g<>n<EFBFBD>r<EFBFBD> automatiquement, les roots sont en copie.
Pour toute demande, merci de faire un ticket <20> admin\@acu.epita.fr
--
Les roots ACU";
my $message = Email::MIME->create(
header_str => [
From => 'root@acu.epita.fr',
To => $entry->get_value("mailAlias"),
Cc => 'root@acu.epita.fr',
Subject => '[LAB][SSH-PASSPHRASE] Clef SSH non prot<6F>g<EFBFBD>e',
],
attributes => {
encoding => 'quoted-printable',
charset => 'UTF-8',
},
body_str => $body,
);
# send the message
use Email::Sender::Simple qw(sendmail);
sendmail($message);
};
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 d<>couvert une clef sans passphrase sur votre
compte du laboratoire.
N'ayant pas corrig<69> votre situation apr<70>s plusieurs relances, nous avons
d<EFBFBD>sactiv<EFBFBD> votre compte et supprim<69> le(s) clef(s) incrimin<69>es.
Pour information, voici l'empreinte de chacune des clefs supprim<69>e :\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 g<>n<EFBFBD>r<EFBFBD> automatiquement, les roots sont en copie.
Pour toute demande, merci de faire un ticket <20> admin\@acu.epita.fr
--
Les roots ACU";
my $message = Email::MIME->create(
header_str => [
From => 'root@acu.epita.fr',
To => $entry->get_value("aliasmail"),
Cc => 'root@acu.epita.fr',
Subject => '[LAB][SSH-PASSPHRASE] Cl<43> SSH non prot<6F>g<EFBFBD>e supprim<69>e',
],
attributes => {
encoding => 'quoted-printable',
charset => 'UTF-8',
},
body_str => $body,
);
# send the message
use Email::Sender::Simple qw(sendmail);
sendmail($message);
};
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