#!/usr/bin/python # # Copyright (C) 2005 Dominik Epple # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # # $Header$ # import pwd,getopt,sys,ldif,os,os.path,tempfile,time,math,re,popen2 minuid=10000 maxuid=60000 homepath='/home/ptwap/c' ldap_base="dc=tphys,dc=physik,dc=uni-tuebingen,dc=de" ldap_users_ou="ou=People"+","+ldap_base ldap_users_classes=['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount', 'shadowAccount', 'ptwapUser', 'top'] krb_realm='TPHYS.PHYSIK.UNI-TUEBINGEN.DE' default_shell='/bin/bash' default_editor="/usr/bin/vi" editor="" default_mail="%s@tphys.physik.uni-tuebingen.de" ldap_proposed_attrs={ # NB! the dn is not an attribute #"dn":1, "objectClass":100, "cn":110, "sn":120, "givenName":130, # posix account attributes "uid":200, "uidNumber":210, "gidNumber":220, "gecos":230, "homeDirectory":240, "loginShell":250, "shadowExpire":260, # sysadmin attributes "manager":300, "privateEmail":310, # web attributes "webShow":400, "employeeType":410, "mail":420, "roomNumber":430, "telephoneNumber":440, "title":450, "labeledURI":460, "webDescription":470, # legacy attributes "description":500, "preferredEmail":510 } # attributes that are not to be changed after the check() session ldap_immutable_attrs=["uid", "uidNumber", "homeDirectory"] def daysSinceJan1st1970(t=None): if(t != None): f=t.split('-') secs=time.mktime((int(f[0]), int(f[1]), int(f[2]), 0, 0, 0, 0, 0, 0)) else: tm=time.localtime() secs=time.mktime((tm[0], tm[1], tm[2], 0, 0, 0, 0, 0, 0)) secs=secs/86400 return int(math.ceil(secs)) def daysSinceJan1st1970inv(days=None): if(days == None): secs=time.time() else: secs=days*86400 tm=time.localtime(secs) return "%04d-%02d-%02d" % (tm[0], tm[1], tm[2]) def usage(msg=None, exitvalue=1): if not msg == None: sys.stderr.write(msg+"\n") sys.stderr.write("\nUsage: "+sys.argv[0]+" [options] \n") sys.stderr.write("""Options are: -C, --convert Convert an existing account -D, --delete Delete an existing account -M, --Modify Modify existing LDAP account -u, --uidNumber Select numerical user id -g, --gidNumber Select numerical group id -s, --loginShell Select login shell -h, --homeDirectory Home directory -n, --cn Select login shell --title LDAP attribute 'title' -e, --shadowExpire Account expiry date (YYYY-MM-DD format) -?, --help Display this help message uid of the account to be created """) sys.exit(exitvalue) class SortingLDIFWriter(ldif.LDIFWriter): def __init__(self,output_file,base64_attrs=None,cols=76,line_sep='\n',key_sorter=cmp): """ key_sorter Function to sort keys with all other arguments are passed to the base class """ ldif.LDIFWriter.__init__(self,output_file,base64_attrs,cols,line_sep) self.key_sorter = key_sorter def _unparseEntryRecord(self,entry): """ entry dictionary holding an entry """ attr_types = entry.keys()[:] attr_types.sort(self.key_sorter) for attr_type in attr_types: for attr_value in entry[attr_type]: self._unparseAttrTypeandValue(attr_type,attr_value) def _writeEntryToFileObject(entry, file): ldif_writer=SortingLDIFWriter(file, \ key_sorter=lambda a,b: cmp(ldap_proposed_attrs.get(a,10000),ldap_proposed_attrs.get(b,10000))) dn='uid='+entry['uid'][0]+","+ldap_users_ou ldif_writer.unparse(dn,entry) def _readEntryFromFileObject(file): reclist=ldif.LDIFRecordList(file) reclist.parse() return reclist def _readLDAPEntry(uid): cmd='ldapsearch -Y GSSAPI -Q -LLL -b %s \'(uid=%s)\'' % (ldap_users_ou, uid) inst=popen2.Popen3(cmd, True) inst.tochild.close() reclist=_readEntryFromFileObject(inst.fromchild) inst.fromchild.close() for line in inst.childerr: sys.stdout.write("err> %s" % line) inst.childerr.close() retval=inst.wait() modified_dn, modified_entry=reclist.all_records[0] return modified_entry def _interactiveLDAPModify(cmd, entry): global editor pattern=re.compile('\s*additional info: (.*)') dn="uid=%s,%s" %(entry['uid'][0],ldap_users_ou) while 1: cmdline='%s -Y GSSAPI -Q' % cmd inst=popen2.Popen3(cmdline, True) _writeEntryToFileObject(entry, inst.tochild) inst.tochild.close() addInfo=None for line in inst.childerr: m=pattern.match(line) if(m!=None): addInfo=m.group(1) inst.fromchild.close() inst.childerr.close() retval=inst.wait() if retval==0: break if os.WIFEXITED(retval): print "%s exited via exit(%s)" % (cmd, os.WEXITSTATUS(retval)) if os.WIFSIGNALED(retval): print "%s caught deadly signal %s" % (cmd, os.WTERMSIG(retval)) if addInfo != None: print addInfo sys.stderr.write('Press to try again or `q\' to quit.\n') ans=sys.stdin.readline().rstrip(os.linesep) if ans == 'q': sys.stderr.write('Giving up on user request.\n') sys.exit(1) templdif_fd, templdif_name=tempfile.mkstemp() templdif_file=os.fdopen(templdif_fd, "w") _writeEntryToFileObject(entry, templdif_file) templdif_file.close() os.system('%s %s' % (editor, templdif_name)) templdif_file=open(templdif_name, 'r') reclist=_readEntryFromFileObject(templdif_file) templdif_file.close() modified_dn, modified_entry=reclist.all_records[0] if modified_dn != dn: print 'Ignoring changes on immutable key `dn\'.' for key in modified_entry.keys(): try: if modified_entry[key] != entry[key]: if key in ldap_immutable_attrs: print 'Ignoring changes on immutable key `%s\'' % key else: entry[key]=modified_entry[key] except KeyError: if key in ldap_immutable_attrs: print 'Ignoring changes on immutable key `%s\'' % key else: entry[key]=modified_entry[key] for key in entry.keys(): if not modified_entry.has_key(key): if key in ldap_immutable_attrs: print 'Ignoring changes on immutable key `%s\'' % key else: del entry[key] def _readUnixPasswdEntry(entry): try: pwent=pwd.getpwnam(entry['uid'][0]) except KeyError: sys.stderr.write("Error: uid "+entry['uid'][0]+":No such login, thus unable to convert!\n") sys.exit(1) if entry.has_key('uidNumber'): usage('Bad option uidNumber in convert mode.') else: entry['uidNumber']=[str(pwent[2])] if entry.has_key('gidNumber'): usage('Bad option gidNumber in convert mode.') else: entry['gidNumber']=[str(pwent[3])] if entry.has_key('cn'): pass else: entry['cn']=[pwent[4]] entry['gecos']=[pwent[4]] nameparts=pwent[4].split(' '); entry['sn']=[nameparts.pop()] entry['givenName'] = [" ".join(nameparts)] if entry.has_key('homeDirectory'): usage('Bad option homeDirectory in convert mode.') else: entry['homeDirectory']=[pwent[5]] if not entry.has_key('loginShell'): entry['loginShell']=[pwent[6]] # Pluggable Configuration Module :) class PCM_LDAP_create: def perform(self,entry): _interactiveLDAPModify('ldapadd', entry) def _findFreeUidNumber(self): pwdEntries=pwd.getpwall() pwdEntries_byUidNumber={} for e in pwdEntries: pwdEntries_byUidNumber[e[2]]=e for uidNumber in range(minuid, maxuid): if(not pwdEntries_byUidNumber.has_key(uidNumber)): break else: sys.stderr.write("Unable to find free uidNumber!\n") sys.exit(1) return str(uidNumber) def check(self,entry): entry['objectClass']=ldap_users_classes if not entry.has_key('uidNumber'): entry['uidNumber']=[self._findFreeUidNumber()] if not entry.has_key('gidNumber'): usage('Please give a group id (gid).') if not entry.has_key('shadowExpire'): usage('Please supply account expiry date.') if not entry.has_key('homeDirectory'): entry['homeDirectory']=[homepath+'/'+entry['uid'][0]] if not entry.has_key('loginShell'): entry['loginShell']=[default_shell] if not entry.has_key('mail'): entry['mail']=[default_mail % entry['uid'][0]] if not entry.has_key('manager'): entry['manager']=['uid=,'+ldap_users_ou] try: pwd.getpwnam(entry['uid'][0]) # I must have found one sys.stderr.write("uid "+entry['uid'][0]+" already exists!\n") sys.exit(1) except KeyError: pass try: pwd.getpwuid(int(entry['uidNumber'][0])) # I must have found one sys.stderr.write("uidNumber "+entry['uidNumber'][0]+" already exists!\n") sys.exit(1) except KeyError: pass for attr in ldap_proposed_attrs.keys(): if not entry.has_key(attr): entry[attr]=[''] class PCM_LDAP_convert: def check(self,entry): self.add=False entry['objectClass']=ldap_users_classes _readUnixPasswdEntry(entry) try: ldap_entry=_readLDAPEntry(entry['uid'][0]) for key in ldap_entry.keys(): if not entry.has_key(key): entry[key]=ldap_entry[key] except IndexError: self.add=True if not entry.has_key('shadowExpire'): usage('Please supply account expiry date.') if not entry.has_key('mail'): entry['mail']=[default_mail % entry['uid'][0]] if not entry.has_key('manager'): entry['manager']=['cn=,'+ldap_users_ou] for attr in ldap_proposed_attrs.keys(): if not entry.has_key(attr): entry[attr]=[''] def perform(self,entry): if self.add: cmd='ldapadd' else: cmd='ldapmodify' _interactiveLDAPModify(cmd, entry) class PCM_LDAP_modify: def check(self,entry): try: ldap_entry=_readLDAPEntry(entry['uid'][0]) for key in ldap_entry.keys(): if not entry.has_key(key): entry[key]=ldap_entry[key] except IndexError: print "Cannot modify %s: no such LDAP account!" sys.exit(1) def perform(self,entry): _interactiveLDAPModify('ldapmodify', entry) class PCM_LDAP_delete: def perform(self,entry): dn='uid='+entry['uid'][0]+","+ldap_users_ou retval=os.system('ldapdelete -Y GSSAPI -Q %s >/dev/null' % dn) if (retval & 0xff) > 0: sys.stderr.write('Fatal error executing os.system(), giving up!\n') sys.exit(1) retval = retval >> 8 if retval > 0: sys.stderr.write('ldapdelete did not complete successfully!\n') def check(self,entry): pass class PCM_Kerberos_create: def perform(self,entry): kadmincmdline='kadmin -l add --random-password --max-ticket-life=1d --pw-expiration-time=never --attributes= --max-renewable-life=1w --expiration-time=%s %s' \ % (daysSinceJan1st1970inv(int(entry['shadowExpire'][0])), entry['uid'][0]) p=re.compile('added %s\@%s with password `([^\']*)\'' % (entry['uid'][0],krb_realm)) pipe=os.popen(kadmincmdline, 'r') success=0 password='x' for line in pipe: m=p.match(line) if(m!=None): password=m.group(1) success=1 if success==1: print "Principal %s with password `%s' successfully created." \ % (entry['uid'][0],password) print "Use `kpasswd %s' to set a different password." % entry['uid'][0] else: print "*** Kerberos update failed!" print " Possible reason for this may be:" print " - malformatted account expiry date" print " - principal already present in Kerberos database" def check(self,entry): if not entry.has_key('shadowExpire'): usage('Please supply account expiry date.') kadmincmdline='kadmin -l list %s' % entry['uid'][0] kadmin_in, kadmin_out, kadmin_err=os.popen3(kadmincmdline, 'r') notfound=0 for line in kadmin_err: if line.find('Principal does not exist') != -1: notfound=1 if notfound==0: sys.stderr.write("Kerberos principal "+entry['uid'][0]+" already exists!\n") sys.exit(1) class PCM_Kerberos_delete: def perform(self,entry): kadmincmdline='kadmin -l delete %s' % entry['uid'][0] retval=os.system(kadmincmdline) if (retval & 0xff) > 0: sys.stderr.write('Fatal error executing os.system(), giving up!\n') sys.exit(1) retval = retval >> 8 if retval > 0: sys.stderr.write('kadmin did not complete successfully!\n') def check(self,entry): pass class PCM_HomeDirectory_create: def perform(self,entry): home=entry['homeDirectory'][0] createstr="mkdir "+home \ +"&& chmod 755 "+home \ +"&& cp -pR /etc/skel/.??* "+home \ +"&& chown -R "+entry['uidNumber'][0]+" "+home \ +"&& chgrp -R "+entry['gidNumber'][0]+" "+home \ +"&& setquota -u -F vfsv0 "+entry['uid'][0]+" 200000 400000 0 0 /mnt/home" \ +"&& setquota -u -F vfsv0 "+entry['uid'][0]+" 20000 40000 0 0 /mnt/mail" os.system(createstr) def check(self,entry): if os.path.exists(entry['homeDirectory'][0]): sys.stderr.write("homeDirectory "+entry['homeDirectory'][0]+" already exists!\n") sys.exit(1) class PCM_HomeDirectory_delete: def check(self,entry): try: uid=entry['uid'][0] except KeyError: sys.stderr.write('Unable to determine user id, aborting.\n') sys.exit(1) try: self.home=pwd.getpwnam(uid)[5] except KeyError: sys.stderr.write('Unable to determine user directory, aborting.\n') sys.exit(1) if not os.path.isdir(self.home): sys.stderr.write('Confused, user home dir is not a directory, aborting.\n') sys.exit(1) def perform(self,entry): sys.stdout.write('About to delete %s. `q\' to abort, Enter to continue.\n' % self.home) ans=sys.stdin.readline().rstrip(os.linesep) if ans == 'q': sys.stderr.write('Giving up on user request.\n') sys.exit(1) os.system('rm -rf %s' % self.home) class PCM_NIS_delete: def check(self,entry): # could do more checking pass def perform(self,entry): uid=entry['uid'][0] for file in ['/var/yp/source/passwd', '/var/yp/source/shadow']: # if($backup ne "") { # my @paths=split(/\//, $file); # my $base=pop @paths; # my $backupfile="/usr/local/var/useradd-krb-ldap/backup/NIS/$login.$base"; # system "grep -E '^$login:' $file > $backupfile"; # } suffix=".%s.%s" % (time.time(),os.getpid()) os.system("sed -i%s -e '/^%s:/d' %s" % (suffix, uid, file)) os.system("make -s -C /var/yp"); def main(): global editor try: opts, args = getopt.getopt(sys.argv[1:], "?u:g:n:h:s:e:DCM", ["help", "uidNumber=", "gidNumber=", "cn=", "homeDirectory=", "loginShell=", "shadowExpire=", "delete", "convert", "modify", "title="]) except getopt.GetoptError: # print help information and exit: usage("Argument parsing error (GetoptError)") newuser={} mode='create' for o, a in opts: if o in ("-?", "--help"): usage(exitvalue=0) elif o in ("-D", "--delete"): mode='delete' elif o in ("-C", "--convert"): mode='convert' elif o in ("-M", "--modify"): mode='modify' elif o in ("-u", "--uidNumber"): newuser['uidNumber'] = [a] elif o in ("-g", "--gidNumber"): newuser['gidNumber'] = [a] elif o in ("-h", "--homeDirectory"): newuser['homeDirectory'] = [a] elif o in ("-s", "--loginShell"): newuser['loginShell'] = [a] elif o in ("-n", "--cn"): newuser['cn']=[a] newuser['gecos']=[a] nameparts=a.split(' '); newuser['sn']=[nameparts.pop()] newuser['givenName'] = [" ".join(nameparts)] elif o in ("-e", "--shadowExpire"): newuser['shadowExpire'] = [str(daysSinceJan1st1970(a))] elif o in ("--title"): newuser['title'] = [a] else: usage('Unknown option '+o) if len(args) == 0: usage() newuser['uid']=[args[0]] if os.environ.has_key("EDITOR"): editor=os.environ["EDITOR"] else: editor=default_editor modules=[] if mode=='create': modules.append(PCM_LDAP_create()) modules.append(PCM_Kerberos_create()) modules.append(PCM_HomeDirectory_create()) elif mode=='delete': modules.append(PCM_LDAP_delete()) modules.append(PCM_Kerberos_delete()) modules.append(PCM_HomeDirectory_delete()) elif mode=='convert': modules.append(PCM_LDAP_convert()) modules.append(PCM_Kerberos_create()) modules.append(PCM_NIS_delete()) elif mode=='modify': modules.append(PCM_LDAP_modify()) for module in modules: module.check(newuser) for module in modules: module.perform(newuser) if __name__ == "__main__": main()