#!/usr/bin/perl -w # Copyright 2003 by Zed Pobre # Licensed to the public under the terms of the GNU GPL, version 2 # Heavily modified by intrigeri@boum.org for http://print.squat.net # Latest version is on https://wiki.boum.org/ConnectFr/GensDansLdap # # This is intended to be a replacement for the standard "adduser" # script using a LDAP database for user authentication and information # storage. It requires at a minimum that schemas are provided for : # objectClass: person # objectClass: organizationalPerson # objectClass: inetOrgPerson # objectClass: posixAccount # objectClass: shadowAccount # Exit codes: # # 0: Success # 1: User specified showed up more than once in the database # 2: LDAP add (user) failed # 3: No username specified, or showhelp was called. # 4: Range from $firstuid to $lastuid was full and no $uid manually specified. # 5: Adduser was asked to create a home directory, but wasn't running as root. # 6: le gidNumber spécifié apparaît plus d'une fois dans la base # 7: LDAP add (group) failed # Changelog # # intrigeri - 2003 09 25 - version 1.1 # - remplacé l'objectClass 'account' par les objectClass 'person', # 'organizationalPerson', 'inetOrgPerson' # intrigeri - 2003 07 25 - version 1.0 # - l'utilisateurice est maintenant ajoutéE à son groupe de base # intrigeri - 2003 07 16 - version 0.3 # - suppresion du bindpass qui était inutile et dangereux # - ajout d'un groupe par défaut = $usersgid # intrigeri - 2003 07 01 - version 0.2 # - un user a maintenant pour objectClass: account, posixAccount, # shadowAccount # - les $HOME sont maintenant dans /home/nfs/ # - les uids sont attribués à partir de 2000 pour éviter le recoupement # avec les uids locaux # TODO # - ajouter le nouvel user à d'autres groupes que son groupe de base # - manage opt_quiet, STDERR and print less erraticaly ;) # - ask, if gid != users, if the new user should be added to group users # - check if ldap->unbind is called each time the program exits use strict; use warnings; use English; use Digest::MD5 qw(md5_base64); use Getopt::Long qw(:config bundling); # the "bundling" option sets short options case sensitive use Net::LDAP; use Net::LDAP::Entry; use Term::ReadPassword; # General Configuration Options my $version = 1.1; my $dshell = "/bin/bash"; # Default Shell my $dhome = "/home/nfs"; # Default home directory base my $grouphomes = 0; # Create home directories as /home/groupname/user my $letterhomes = 0; # Create home directories as /home/u/user my $maildir = "/var/mail"; # Where mail spool files are stored my $skel = "/etc/skel"; # Directory with skeletal user files my $firstsystemuid = 100; my $lastsystemuid = 999; my $firstsystemgid = 100; my $lastsystemgid = 999; my $firstuid = 2000; my $lastuid = 19999; my $usergroups = 0; my $usersgid = 100; my $quotauser; my $ddirmode = "0700"; # Default home directory permissions my $setgidhome = 0; my $detail = 0; # 0 = Bare minimum: ask only for password # 1 = Ask for full GECOS information # 2 = Ask for extended information my $generatepassword; # System command used to generate a password $generatepassword = "passwdgen -aA1\@pq"; # LDAP Variables my $ldap; # LDAP connection my $ldaphost; # LDAP server FQDN or IP address my $ldapversion;# LDAP version (2 or 3) my $userfilter; # Filter string to show just user account entries. my $groupfilter;# Filter string to show just group entries my $result; # Error handling variable my $search; # Stores results from LDAP searches my $entry; # Individual LDAP entry my $bindcn; # CN to bind with to authorize adding an entry my $bindpass; # LDAP Bind Password my $base; # Base under which everything is stored my $userou; # Organizational unit under which user accounts are stored my $groupou; # Organizational unit under which group information is stored my $userbase; # Complete search base for searching for users my $groupbase; # Complete search base for searching for groups my $cn; # User entry cn $ldaphost = "127.0.0.1"; $ldapversion = 3; $userfilter = "(objectClass=posixAccount)"; $groupfilter = "(objectClass=posixGroup)"; $bindcn = "cn=admin,dc=tanneries,dc=taz"; $base = "dc=tanneries,dc=taz"; $userou = "ou=People"; $groupou = "ou=Group"; if(defined $userou) { $userbase = $userou.",".$base; } else { $userbase = $base; } if(defined $groupou) { $groupbase = $groupou.",".$base; } else { $groupbase = $base; } # User data variables my $username; # Username my $uidnumber; # Numeric user id my $gidnumber; # Numeric group id my $groupinfo; # Group information from User::grent; my $group; # Group name matching group ID my $shell; # Login shell my $homedir; # Home directory my $dirmode; # Home directory permissions my $password1; # First password input my $password2; # Repeated password input my $password; # Password string ready for entry into the LDAP system my %accounts; # Hash containing username/password pairs, for use in a summary. # Potential command line variables my $opt_conf; my $opt_disabledlogin; my $opt_disabledpassword; my $opt_forcebadname; my $opt_gid; my $opt_help; my $opt_home; my $opt_shell; my $opt_ingroup; my $opt_nocreatehome; my $opt_quiet; my $opt_system; my $opt_uid; # This maps to uidNumber, not uid my $opt_firstuid; my $opt_lastuid; my $opt_version; my $opt_nocreatemail; # Do not initialize a mail spool file my $opt_verbose; my $opt_dirmode; my $opt_detail; my $opt_genpass; my $opt_simulate; my $opt_summary; GetOptions("conf=s" => \$opt_conf, "disabled-login" => \$opt_disabledlogin, "disabled-password" => \$opt_disabledpassword, "force-badname" => \$opt_forcebadname, "gid|g=i" => \$opt_gid, "help|h|?" => sub { showhelp() }, "home=s" => \$opt_home, "dirmode=s" => \$opt_dirmode, "shell=s" => \$opt_shell, "ingroup=s" => \$opt_ingroup, "no-create-home" => \$opt_nocreatehome, "no-create-mail" => \$opt_nocreatemail, "quiet|q" => \$opt_quiet, "system" => \$opt_system, "uid|u=i" => \$opt_uid, "firstuid=i" => \$opt_firstuid, "lastuid=i" => \$opt_lastuid, "verbose|v" => \$opt_verbose, "version|V" => \$opt_version, "detail|d=i" => \$opt_detail, "genpass|generate-password" => \$opt_genpass, "simulate|no-act" => \$opt_simulate, "summary|s" => \$opt_summary ); $firstuid = $opt_firstuid if defined $opt_firstuid; $lastuid = $opt_lastuid if defined $opt_lastuid; $detail = $opt_detail if defined $opt_detail; if( (!defined $opt_nocreatehome) && ($EUID != 0) ) { print STDERR "You must have root permissions to create home directories.\n"; exit 5; } # --genpass implies --summary, otherwise there is no way to see the # passwords generated. $opt_summary = 1 if defined $opt_genpass; # After the option parsing is finished, each leftover argument is a # username to be handled. Those get shifted out of the @ARGV array # one at a time into the scalar variable $username and then processed. # # Do not be confused by the use of "uid" in LDAP filters -- in the # LDAP database uid refers to the username, uidNumber refers to the # numeric user ID, and gidNumber refers to the numeric group ID. # # If @ARGV contains no remaining entries, print help and exit # if(scalar(@ARGV) == 0) { print STDERR "You must specify a username.\n\n"; showhelp(); exit 3; # This is redundant, but is probably a good sanity-check } $ldap = Net::LDAP->new($ldaphost,version => $ldapversion) or die $!; $result = $ldap->start_tls(); die $result->error() if $result->code(); $bindpass = read_password("LDAP Bind Password: "); $result = $ldap->bind($bindcn, password => $bindpass); die $result->error() if $result->code(); while(@ARGV) { $username = shift @ARGV; $search = $ldap->search(base => $userbase, scope => "one", filter => "(&(uid=".$username.")".$userfilter.")" ); die $search->error() if $search->code(); if($search->count > 1) { print STDERR "More than one entry found for (&(uid=$username)$userfilter)\n"; print STDERR "This should not happen. Please check that the LDAP filter you specified\n"; print STDERR "is correct, and that your database is not corrupted.\n"; exit 1; } if($search->count) { print STDERR "The specified username ($username) already exists in the database. Skipping.\n"; next; } if(defined $opt_genpass) { $password1 = `$generatepassword`; $password = "{MD5}".md5_base64($password1)."=="; } elsif( !defined($opt_disabledlogin) && !defined($opt_disabledpassword) ) { { # Begin block for redo to be able to ask for a password several times on mismatch. $password1 = read_password("User Password: "); if(!defined $password1) { print STDERR "Failed to get a password. Try again.\n"; redo; } $password2 = read_password("User Password (again): "); if($password1 ne $password2) { print STDERR "Password mismatch. Try again.\n"; redo; } # The "==" is appended to pad the digest string so that it is # a multiple of four bytes. This is necessary for # interoperability with other Base64 md5 digest strings as # described in Digest::MD5. $password = "{MD5}".md5_base64($password1)."=="; } } else { $password = " "; } if(defined $opt_uid) { $uidnumber = $opt_uid; } else { print STDERR "Looking for uidNumber between ".$firstuid." and ".$lastuid."\n" if defined $opt_verbose; $uidnumber = getuidnumber(); } if(defined $opt_gid) { print STDERR "Getting group information from --gid.\n" if defined $opt_verbose; $gidnumber = $opt_gid; $group = getgrgid($gidnumber) or die "getgrgid($gidnumber) failure"; if(!defined $group) { print STDERR "WARNING: invalid gidnumber (".$opt_gid.") provided. Discarding.\n"; undef $gidnumber; } # I don't know why this is necessary. If you fail to do # this and you have more than one account to add, as this # function is hit for the next user, the script will silently # abort on the getgrgid call returning value 141. undef $opt_gid; # print "DEBUG: exiting if(defined \$opt_gid)\n"; } if(defined $opt_ingroup) { print STDERR "Getting group information from --ingroup.\n" if defined $opt_verbose; $group = $opt_ingroup; (undef,undef,$gidnumber,undef) = getgrnam($group); if(!defined $gidnumber) { print STDERR "WARNING: invalid groupname (".$opt_ingroup.") provided. Discarding.\n"; undef $group; } } if( (!defined($group)) || (!defined($gidnumber)) ) { { # Begin block for redo to ask for a valid group. print "gidNumber (? for list, RETURN for users): "; $gidnumber = ; chomp $gidnumber; if($gidnumber eq "?") { showgroups(); redo; } if($gidnumber eq "") { $gidnumber = $usersgid; } } $group = getgrgid($gidnumber); if(!defined $group) { print STDERR "ERROR: nonexistent group: ".$gidnumber." -- try again.\n"; redo; } } $homedir = $dhome."/".$username; ($shell,$dirmode) = getinfo(); $cn = $username; # We should now know everything that we're going to know. # Set up the entry for values that must exist $entry = Net::LDAP::Entry->new; $entry->dn("uid=".$username.",".$userbase); $entry->add(userid => $username, cn => $cn, sn => $cn, objectClass => "top", objectClass => "person", objectClass => "organizationalPerson", objectClass => "inetOrgPerson", objectClass => "posixAccount", objectClass => "shadowAccount", userPassword => $password, loginShell => $shell, uidNumber => $uidnumber, gidNumber => $gidnumber, homeDirectory => $homedir ); $entry->dump() if defined $opt_verbose; # Créer l'entrée modifiée pour le gid. # L'écriture effective dans la base sera faite ensuite. # # ALGO # si (l'user créé appartient déjà au groupe) alors # on ne touche pas au groupe # sinon # ajouter un champ memberUid au groupe # fin si $search = $ldap->search(base => $groupbase, scope => "one", filter => "(&(gidNumber=".$gidnumber.")".$groupfilter.")" ); die $search->error() if $search->code(); if($search->count > 1) { print STDERR "More than one entry found for (&(gidNumber=$gidnumber)$groupfilter)\n"; print STDERR "This should not happen. Please check that the LDAP filter you specified\n"; print STDERR "is correct, and that your database is not corrupted.\n"; exit 6; } my $groupentry = $search->entry(0); my $update_group; if( user_in_group($groupentry, $username) ) { $update_group = 0; print STDERR "WARNING: ".$username." is already a member of group ".$gidnumber."\n"; } else { $update_group = 1; $groupentry->changetype('modify'); $groupentry->add(memberUid => $username); } # Écriture effective dans la base, etc. if(!defined $opt_simulate) { $result = $ldap->add($entry); if( $result->code() ) { print STDERR "ERROR: LDAP add (user) returned: ".$result->error()."\n"; $ldap->unbind(); exit 2; } print "Account ".$username." successfully added into the database.\n" if !defined $opt_quiet; if($update_group) { $result = $groupentry->update($ldap); if( $result->code() ) { print STDERR "ERROR: LDAP add (group) returned: ".$result->error()."\n"; $ldap->delete($entry); $ldap->unbind(); exit 7; } print "Account ".$username." successfully added into his group in the database.\n" if !defined $opt_quiet; } if(!defined $opt_nocreatehome) { if(-e $homedir) { if(!defined $opt_quiet) { print STDERR "WARNING: something already exists at ".$homedir."\n"; print STDERR "Not creating home directory.\n"; } } else { system("mkdir ".$homedir); system("cp -r /etc/skel/. ".$homedir."/"); system("chown -R ".$uidnumber.".".$gidnumber." ".$homedir); print STDERR "Home directory created and populated.\n" if !defined $opt_quiet; } } if(!defined $opt_nocreatemail) { if(-e $maildir."/".$username) { if(!defined $opt_quiet) { print STDERR "WARNING: mail spool file already exists at ".$maildir."/".$username."\n"; print STDERR "Not touching mail spool.\n"; } } else { system("touch ".$maildir."/".$username); system("chown ".$username.".mail ".$maildir."/".$username); system("chmod 600 ".$maildir."/".$username); print STDERR "Mail spool initialized.\n" if !defined $opt_quiet; } } } $accounts{$username} = $password1; } if(defined $opt_summary) { print sort map { "$_\t$accounts{$_}" } keys %accounts; } $ldap->unbind(); exit 0; ############################################################################### # # sub getuidnumber # Returns a single integer # sub getuidnumber { my $retval = $firstuid; $search = $ldap->search(base => $userbase, scope => "one", filter => $userfilter, attrs => "uidNumber" ); die $search->error() if $search->code(); print STDERR $search->count." accounts examined\n" if defined $opt_verbose; foreach $entry ( $search->sorted("uidNumber") ) { if($entry->get_value("uidNumber") >= $firstuid) { if($entry->get_value("uidNumber") < $lastuid) { $retval = $entry->get_value("uidNumber")+1; } } } if($retval > $lastuid) { print STDERR "The uidNumber range ".$firstuid." to ".$lastuid." is full. Aborting.\n"; exit 4; } print STDERR "Next available uidNumber: ".$retval."\n" if defined $opt_verbose; return $retval; } # # sub getinfo # # ($shell,$dirmode) = getinfo(); # # Prompts for any needed user information, such as anything normally # asked by chfn. Will check the global $detail to determine how much # to ask. # sub getinfo { my $user_shell; my $user_dirmode; if(defined $opt_shell) { $user_shell = $opt_shell; } elsif( ($detail > 1) && (!defined $opt_quiet) ) { print "Login Shell [".$dshell."]: "; $user_shell = ; chomp $user_shell; $user_shell = $dshell if($user_shell eq ""); } else { $user_shell = $dshell; } if(defined $opt_dirmode) { $user_dirmode = $opt_dirmode; } elsif( ($detail > 1) && (!defined $opt_quiet) ) { print "Home Directory Permissions [".$ddirmode."]: "; $user_dirmode = ; chomp $user_dirmode; $user_dirmode = $ddirmode if($user_dirmode eq ""); } else { $user_dirmode = $ddirmode; } return ($user_shell,$user_dirmode); } # # sub showgroups # # pulls up a list of groups from the LDAP database and displays # GID/Groupname pairs for reference. # # Takes no arguments, returns no values. # sub showgroups { $search = $ldap->search(base => $groupbase, scope => "one", filter => $groupfilter, attrs => ["cn","gidNumber"] ); foreach $entry ( $search->sorted("gidNumber") ) { print $entry->get_value("gidNumber")."\t".$entry->get_value("cn")."\n"; } return; } # # sub showhelp # # Displays a help message displaying usage options and then exits with # an error. # # Takes no arguments, does not return. # sub showhelp { print "To add a normal user:\n"; print "ldapadduser [--home DIR] [--no-create-home] [--dirmode MODE]\n"; print " [--no-create-mail] [--shell SHELL]\n"; print " [--uid ID] [--firstuid ID] [--lastuid ID]\n"; print " [--ingroup GROUP | --gid ID] [--detail #] [--verbose | --quiet]\n"; print " [--disabled-password | --disabled-login | --generate-password]\n"; print " [--simulate] [--summary] username1 username2 username3 ...\n"; print "\n"; exit 3; } # # sub user_in_group # # Takes as arguments: # - a LDAP posixGroup contained in an object Net::LDAP::Entry # - a string storing a user's name # Returns 1 if this user is a member of this group, and 0 else. # sub user_in_group { my $groupentry = shift; my $username = shift; return 0 if(group_is_empty($groupentry)); my @values = $groupentry->get_value('memberUid'); foreach my $value ( @values ) { return 1 if($value eq $username); } return 0; } # # sub group_is_empty # # Takes as argument a LDAP posixGroup contained in an object Net::LDAP::Entry. # Returns 1 if this group has no member, and 0 else. # sub group_is_empty { my $groupentry = pop; my @attributs = $groupentry->attributes(); foreach my $attribut ( @attributs ) { return 0 if($attribut eq 'memberUid') } return 1; }