#!/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; ########################################################### # # # Global variables # # # ########################################################### my $ldaphost = "ldap.acu.epita.fr"; my $ldapuri = "ldaps://ldap.acu.epita.fr"; my $binddn = "cn=admin,dc=acu,dc=epita,dc=fr"; my $bindsecret = ''; my $login = ""; 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 } ); ########################################################### # # # Messages subroutines # # # ########################################################### my $verbosity = 1; my $debug = 0; sub do_err($) { say BOLD, RED, ">>>", RESET, " ", BOLD, @_, RESET; exit(1); } sub do_usage($) { say BOLD, MAGENTA, " * ", RESET, " ", BOLD, @_, RESET; } sub do_warn($) { say BOLD, YELLOW, ">>>", RESET, " ", BOLD, @_, RESET; } sub do_info($) { if ($verbosity) { say BOLD, CYAN, " * ", RESET, " ", @_, RESET; } } sub do_debug($) { if ($debug) { say BOLD, BLUE, " * ", RESET, " ", @_, RESET; } } ########################################################### # # # 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, "open" => \&cmd_account_open, ); 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() { if (defined($ENV{'LDAP_PASSWORD'}) && $ENV{'LDAP_PASSWORD'} ne "") { $bindsecret = $ENV{'LDAP_PASSWORD'}; return; } 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 = ; ReadMode("restore"); print "\n"; chomp $bindsecret; } sub ldap_connect() { if ($bindsecret eq "") { ldap_get_password(); } my $ldap = Net::LDAPS->new($ldaphost) or do_err ("$@"); my $mesg = $ldap->bind($binddn, password => $bindsecret) or do_err ("$@"); if ($mesg->code) { die "An error occurred: " .ldap_error_text($mesg->code)."\n"; } return $ldap; } sub ldap_connect_anon() { my $ldap = Net::LDAPS->new($ldaphost) or do_err ("$@"); my $mesg = $ldap->bind or do_err ("$@"); if ($mesg->code) { die "An error occurred: " .ldap_error_text($mesg->code)."\n"; } return $ldap; } ###################################### # # # ACCOUNT BLOCK # # # ###################################### sub cmd_account(@) { my $login = shift; if (! $login) { do_usage "lpt account [arguments ...]"; return 1; } my $subcmd = shift // "view"; if (! $subcmd) { pod2usage(-verbose => 99, -sections => [ 'ACCOUNT COMMANDS' ] ); } elsif (! exists $cmds_account{$subcmd}) { do_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) { do_usage (" account close"); return -1; } my $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) { do_err $mesg->error; } if ($mesg->count != 1) { do_err "User $login not found or multiple presence"; } if (grep { "epitaAccount" } $mesg->entry(0)->get_value("objectClass")) { do_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")) { do_debug "Setting shell for $login ..."; cmd_account_shell($login, "/bin/false"); } do_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) { do_usage "lpt account create [nopass|passgen|password]"; return 1; } ldap_get_password(); my $group = shift; my $uid = shift; my $firstname = shift; my $lastname = shift; my $pass = shift // "nopass"; my $ldif = <<"EOF"; dn: uid=$login,ou=$group,ou=users,dc=acu,dc=epita,dc=fr objectClass: epitaAccount cn: $firstname $lastname mail: $login\@epita.fr uid: $login uidNumber: $uid EOF open(LDIF, "|-", "ldapadd -x -H '$ldapuri' -w '$bindsecret' -D '$binddn'") || do_err("error !\n"); say LDIF $ldif; close(LDIF); if ($? == 0) { do_info("Account added: $login"); return cmd_account($login, $pass) if ($pass ne "nopass"); return 0; } else { do_err("Unable to add: $login"); } } sub cmd_account_mail(@) { return cmd_account_vieworchange('mail', 'mail', @_); } sub cmd_account_nopass($@) { my $login = shift; my $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) { do_err $mesg->error; } if ($mesg->count != 1) { do_err "User $login not found"; } my $pass = $mesg->entry(0)->get_value("userPassword"); if (! $pass || $pass eq "{crypt}!toto") { $mesg = $ldap->unbind; do_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") { do_debug "y response expected to continue; leaving."; do_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) { do_err $mesg->error; } if ($mesg->count != 1) { do_err "User $login not found"; } $mesg->entry(0)->replace("userPassword" => "{crypt}!toto"); $mesg->entry(0)->update($ldap); do_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) { do_usage "lpt account passgen [nb_char>=10]"; return 1; } printf(STDERR "Are you sure you want to change password for $login? [y/N] "); my $go = ; chomp $go; if ($go ne "y" and $go ne "yes") { do_debug "y response expected to continue, leaving."; do_warn "Password unchanged for $login."; return 2; } do_debug "Generating a $nb_char chars password..."; my $pass = ""; open (HANDLE, "pwgen -s -n -c -y -1 $nb_char 1 |"); while() { $pass = $_; } close(HANDLE); chomp($pass); do_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) { do_usage "lpt account password [new_password]"; return 1; } my $pass = shift; if (! $pass) { say "Changing password for $login."; ReadMode("noecho"); print "new password: "; my $pass1 = ; print "\nretype new password: "; my $pass2 = ; ReadMode("restore"); print "\n"; do_debug "Read passwords: $pass1 and $pass2"; $pass1 eq $pass2 || do_err "Passwords did not match."; $pass = $pass1; } if ($pass eq "") { do_err "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_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) { do_err $mesg->error; } if ($mesg->count != 1) { do_err "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) { do_usage (" account reopen"); return 1; } my $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) { do_err $mesg->error; } if ($mesg->count != 1) { do_err "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 =~ /^\{[^\}]+\}!/) { do_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")) { do_debug "Setting shell for $login ..."; cmd_account_shell($login, $shellValid); } do_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")) { do_usage " account $typeName [list|add|del|flush] [string]"; return 1; } my $ldap; $ldap = ldap_connect() if ($action ne "list"); $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) { do_err $mesg->error; } if ($mesg->count != 1) { do_err "User $login not found or multiple presence"; } if ($action eq "add") { do_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 $!; do_info "Done!"; } else { do_warn "$login has already $change $typeName."; } } elsif ($action eq "del") { do_info "Checking if $change is a ".$typeName."s of $login ..."; my @data = $mesg->entry(0)->get_value($type); if (grep(/^$change$/, @data)) { do_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 $!; do_info "Done!"; } else { do_warn "$change is not a $typeName for $login."; } } elsif ($action eq "flush") { $ldap->modify($mesg->entry(0)->dn, delete => [$type]); do_info "$login have no more $typeName."; } else { if ($mesg->entry(0)->get_value($type)) { do_info $login."'s ".$typeName."s are:"; for my $val ($mesg->entry(0)->get_value($type)) { say " - $val"; } } else { do_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) { do_usage (" account $typeName [new_string]"); return 1; } my $change = shift; my $ldap; $ldap = ldap_connect() if ($change); $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) { do_err $mesg->error; } if ($mesg->count != 1) { do_err "User $login not found or multiple presence"; } if ($change) { do_info "Setting $typeName to $change for $login ..."; $mesg->entry(0)->replace($type => $change) or die $!; $mesg->entry(0)->update($ldap) or die $!; do_info ("Done!"); } else { do_info $login."'s $typeName is ".$mesg->entry(0)->get_value($type)."."; } $ldap->unbind or die ("couldn't disconnect correctly"); return 0; } sub cmd_account_view($@) { my $login = shift; my $ldap = ldap_connect_anon(); my $mesg = $ldap->search(base => "ou=users,dc=acu,dc=epita,dc=fr", filter => "uid=$login", attrs => ['objectClass']); $mesg->code && do_err $mesg->error; if ($mesg->count <= 0) { do_err "No such account!"; } do_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'; } } do_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) { do_usage "lpt group [arguments ...]"; return 1; } my $subcmd = shift // "view"; if (! $subcmd) { pod2usage(-verbose => 99, -sections => [ 'GROUP COMMANDS' ] ); } elsif (! exists $cmds_group{$subcmd}) { do_usage "Unknown command for group: ". $subcmd; return 1; } return $cmds_group{$subcmd}($gname, @_); } sub cmd_group_list(@) { if ($#ARGV > 0) { do_usage (" group list [group]"); exit(1); } my $group = $ARGV[0]; my $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) { do_usage (" group add "); exit(1); } my $group = $ARGV[0]; my $login = $ARGV[1]; my $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) { do_usage (" group remove "); exit(1); } my $group = $ARGV[0]; my $login = $ARGV[1]; my $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 ($#ARGV != 1) { do_usage (" group create "); exit(1); } ldap_get_password(); my $ldif = ""; my $type = $ARGV[0]; my $year = $ARGV[1]; my $group = $type . $year; my $ldif_path = dirname(__FILE__) . "/base-group.ldif"; my $gid; if ($type eq "acu") { $gid = $year; } elsif ($type eq "yaka") { $gid = $year - 1000; } else { print "Error: type must be acu or yaka!"; exit(1); } open(TEMPLATE, $ldif_path) or do_err("unable to open template."); while (