GenPasswd
[mirror/userdir-ldap.git] / ud-generate
1 #!/usr/bin/env python
2 # -*- mode: python -*-
3 # Generates passwd, shadow and group files from the ldap directory.
4
5 #   Copyright (c) 2000-2001  Jason Gunthorpe <jgg@debian.org>
6 #   Copyright (c) 2003-2004  James Troup <troup@debian.org>
7 #   Copyright (c) 2004-2005,7  Joey Schulze <joey@infodrom.org>
8 #   Copyright (c) 2001-2007  Ryan Murray <rmurray@debian.org>
9 #   Copyright (c) 2008,2009,2010 Peter Palfrader <peter@palfrader.org>
10 #   Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
11 #   Copyright (c) 2008 Mark Hymers <mhy@debian.org>
12 #   Copyright (c) 2008 Luk Claes <luk@debian.org>
13 #   Copyright (c) 2008 Thomas Viehmann <tv@beamnet.de>
14 #   Copyright (c) 2009 Stephen Gran <steve@lobefin.net>
15 #   Copyright (c) 2010 Helmut Grohne <helmut@subdivi.de>
16 #
17 #   This program is free software; you can redistribute it and/or modify
18 #   it under the terms of the GNU General Public License as published by
19 #   the Free Software Foundation; either version 2 of the License, or
20 #   (at your option) any later version.
21 #
22 #   This program is distributed in the hope that it will be useful,
23 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
24 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25 #   GNU General Public License for more details.
26 #
27 #   You should have received a copy of the GNU General Public License
28 #   along with this program; if not, write to the Free Software
29 #   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
30
31 import string, re, time, ldap, getopt, sys, os, pwd, posix, socket, base64, sha, shutil, errno, tarfile, grp
32 from userdir_ldap import *
33 from userdir_exceptions import *
34 import UDLdap
35 try:
36    from cStringIO import StringIO
37 except ImportError:
38    from StringIO import StringIO
39
40 global Allowed
41 global CurrentHost
42
43 if os.getuid() == 0:
44    sys.stderr.write("You should probably not run ud-generate as root.\n")
45    sys.exit(1)
46
47 PasswdAttrs = None
48 DebianUsers = None
49 DisabledUsers = []
50 GroupIDMap = {}
51 SubGroupMap = {}
52 Allowed = None
53 CurrentHost = ""
54
55 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
56
57 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
58 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
59 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
60 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
61 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
62 isSSHFP = re.compile("^\s*IN\s+SSHFP")
63 DNSZone = ".debian.net"
64 Keyrings = ConfModule.sync_keyrings.split(":")
65
66 def safe_makedirs(dir):
67    try:
68       os.makedirs(dir)
69    except OSError, e:
70       if e.errno == errno.EEXIST:
71          pass
72       else:
73          raise e
74
75 def safe_rmtree(dir):
76    try:
77       shutil.rmtree(dir)
78    except OSError, e:
79       if e.errno == errno.ENOENT:
80          pass
81       else:
82          raise e
83
84 def Sanitize(Str):
85    return Str.translate(string.maketrans("\n\r\t", "$$$"))
86
87 def DoLink(From, To, File):
88    try: 
89       posix.remove(To + File)
90    except: 
91       pass
92    posix.link(From + File, To + File)
93
94 def IsRetired(DnRecord):
95    """
96    Looks for accountStatus in the LDAP record and tries to
97    match it against one of the known retired statuses
98    """
99
100    status = GetAttr(DnRecord, "accountStatus", None)
101    if status is None:
102       return False
103
104    line = status.split()
105    status = line[0]
106    
107    if status == "inactive":
108       return True
109
110    elif status == "memorial":
111       return True
112
113    elif status == "retiring":
114       # We'll give them a few extra days over what we said
115       age = 6 * 31 * 24 * 60 * 60
116       try:
117          return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
118       except IndexError:
119          return False
120       except ValueError:
121          return False
122
123    return False
124
125 def IsGidDebian(x):
126    try:
127       return int(GetAttr(x, "gidNumber", 0)) == 800
128    except ValueError:
129       return False
130
131 # See if this user is in the group list
132 def IsInGroup(DnRecord):
133   if Allowed is None:
134      return True
135
136   # See if the primary group is in the list
137   if Allowed.has_key(GetAttr(DnRecord, "gidNumber")) != 0:
138      return True
139
140   # Check the host based ACL
141   if DnRecord[1].has_key("allowedHost") != 0:
142      if CurrentHost in DnRecord[1]["allowedHost"]:
143         return True
144
145   # See if there are supplementary groups
146   if DnRecord[1].has_key("supplementaryGid") == 0:
147      return False
148
149   supgroups=[]
150   addGroups(supgroups, DnRecord[1]["supplementaryGid"], GetAttr(DnRecord, "uid"))
151   for g in supgroups:
152      if Allowed.has_key(g):
153         return True
154   return False
155
156 def Die(File, F, Fdb):
157    if F != None:
158       F.close()
159    if Fdb != None:
160       Fdb.close()
161    try: 
162       os.remove(File + ".tmp")
163    except:
164       pass
165    try: 
166       os.remove(File + ".tdb.tmp")
167    except: 
168       pass
169
170 def Done(File, F, Fdb):
171    if F != None:
172       F.close()
173       os.rename(File + ".tmp", File)
174    if Fdb != None:
175       Fdb.close()
176       os.rename(File + ".tdb.tmp", File + ".tdb")
177
178 # Generate the password list
179 def GenPasswd(File, HomePrefix, PwdMarker):
180    F = None
181    try:
182       F = open(File + ".tdb.tmp", "w")
183      
184       userlist = {}
185       # Fetch all the users
186       global PasswdAttrs
187
188       i = 0
189       for x in PasswdAttrs:
190          a = UDLdap.Account(x[0], x[1])
191          if not IsInGroup(x): continue
192
193          # Do not let people try to buffer overflow some busted passwd parser.
194          if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
195
196          userlist[a['uid']] = a['gidNumber']
197          line = "%s:%s:%d:%d:%s:%s%s:%s" % (
198                  a['uid'],
199                  PwdMarker,
200                  a['uidNumber'],
201                  a['gidNumber'],
202                  a['gecos'],
203                  HomePrefix, a['uid'],
204                  a['loginShell'])
205          line = Sanitize(line) + "\n"
206          F.write("0%u %s" % (i, line))
207          F.write(".%s %s" % (a['uid'], line))
208          F.write("=%d %s" % (a['uidNumber'], line))
209          i = i + 1
210
211    # Oops, something unspeakable happened.
212    except:
213       Die(File, None, F)
214       raise
215    Done(File, None, F)
216
217    # Return the list of users so we know which keys to export
218    return userlist
219
220 # Generate the shadow list
221 def GenShadow(File):
222    F = None
223    try:
224       OldMask = os.umask(0077)
225       F = open(File + ".tdb.tmp", "w", 0600)
226       os.umask(OldMask)
227      
228       # Fetch all the users
229       global PasswdAttrs
230      
231       i = 0
232       for x in PasswdAttrs:
233          a = UDLdap.Account(x[0], x[1])
234          if not IsInGroup(x): continue
235
236          # If the account is locked, mark it as such in shadow
237          # See Debian Bug #308229 for why we set it to 1 instead of 0
238          if not a.pw_active():     ShadowExpire = '1'
239          elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
240          else:                     ShadowExpire = ''
241
242          values = []
243          values.append(a['uid'])
244          values.append(a.get_password())
245          for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
246             if key in a: values.append(a[key])
247             else:        values.append('')
248          values.append(ShadowExpire)
249          line = ':'.join(values)+':'
250          line = Sanitize(line) + "\n"
251          F.write("0%u %s" % (i, line))
252          F.write(".%s %s" % (a['uid'], line))
253          i = i + 1
254
255    # Oops, something unspeakable happened.
256    except:
257       Die(File, None, F)
258       raise
259    Done(File, None, F)
260
261 # Generate the sudo passwd file
262 def GenShadowSudo(File, untrusted):
263    F = None
264    try:
265       OldMask = os.umask(0077)
266       F = open(File + ".tmp", "w", 0600)
267       os.umask(OldMask)
268      
269       # Fetch all the users
270       global PasswdAttrs
271      
272       for x in PasswdAttrs:
273          a = UDLdap.Account(x[0], x[1])
274          Pass = '*'
275          if not IsInGroup(x): continue
276      
277          if 'sudoPassword' in a:
278             for entry in a['sudoPassword']:
279                Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
280                if Match == None:
281                   continue
282                uuid = Match.group(1)
283                status = Match.group(2)
284                hosts = Match.group(3)
285                cryptedpass = Match.group(4)
286      
287                if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
288                   continue
289                for_all = hosts == "*"
290                for_this_host = CurrentHost in hosts.split(',')
291                if not (for_all or for_this_host):
292                   continue
293                # ignore * passwords for untrusted hosts, but copy host specific passwords
294                if for_all and untrusted:
295                   continue
296                Pass = cryptedpass
297                if for_this_host: # this makes sure we take a per-host entry over the for-all entry
298                   break
299             if len(Pass) > 50:
300                Pass = '*'
301      
302          Line = "%s:%s" % (a['uid'], Pass)
303          Line = Sanitize(Line) + "\n"
304          F.write("%s" % (Line))
305   
306    # Oops, something unspeakable happened.
307    except:
308       Die(File, F, None)
309       raise
310    Done(File, F, None)
311
312 # Generate the shadow list
313 def GenSSHShadow():
314    # Fetch all the users
315    userfiles = []
316
317    global PasswdAttrs
318
319    safe_rmtree(os.path.join(GlobalDir, 'userkeys'))
320    safe_makedirs(os.path.join(GlobalDir, 'userkeys'))
321
322    for x in PasswdAttrs:
323       a = UDLdap.Account(x[0], x[1])
324       if not 'sshRSAAuthKey' in a: continue
325
326       F = None
327       try:
328          OldMask = os.umask(0077)
329          File = os.path.join(GlobalDir, 'userkeys', a['uid'])
330          F = open(File + ".tmp", "w", 0600)
331          os.umask(OldMask)
332
333          for I in a['sshRSAAuthKey']:
334             MultipleLine = "%s" % I
335             MultipleLine = Sanitize(MultipleLine) + "\n"
336             F.write(MultipleLine)
337
338          Done(File, F, None)
339          userfiles.append(os.path.basename(File))
340
341       # Oops, something unspeakable happened.
342       except IOError:
343          Die(File, F, None)
344          # As neither masterFileName nor masterFile are defined at any point
345          # this will raise a NameError.
346          Die(masterFileName, masterFile, None)
347          raise
348
349    return userfiles
350
351 def GenSSHtarballs(userlist, SSHFiles, grouprevmap, target):
352    OldMask = os.umask(0077)
353    tf = tarfile.open(name=os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
354    os.umask(OldMask)
355    for f in userlist.keys():
356       if f not in SSHFiles:
357          continue
358       # If we're not exporting their primary group, don't export
359       # the key and warn
360       grname = None
361       if userlist[f] in grouprevmap.keys():
362          grname = grouprevmap[userlist[f]]
363       else:
364          try:
365             if int(userlist[f]) <= 100:
366                # In these cases, look it up in the normal way so we
367                # deal with cases where, for instance, users are in group
368                # users as their primary group.
369                grname = grp.getgrgid(userlist[f])[0]
370          except Exception, e:
371             pass
372
373       if grname is None:
374          print "User %s is supposed to have their key exported to host %s but their primary group (gid: %d) isn't in LDAP" % (f, CurrentHost, userlist[f])
375          continue
376
377       to = tf.gettarinfo(os.path.join(GlobalDir, 'userkeys', f), f)
378       # These will only be used where the username doesn't
379       # exist on the target system for some reason; hence,
380       # in those cases, the safest thing is for the file to
381       # be owned by root but group nobody.  This deals with
382       # the bloody obscure case where the group fails to exist
383       # whilst the user does (in which case we want to avoid
384       # ending up with a file which is owned user:root to avoid
385       # a fairly obvious attack vector)
386       to.uid = 0
387       to.gid = 65534
388       # Using the username / groupname fields avoids any need
389       # to give a shit^W^W^Wcare about the UIDoffset stuff.
390       to.uname = f
391       to.gname = grname
392       to.mode  = 0400
393
394       contents = file(os.path.join(GlobalDir, 'userkeys', f)).read()
395       lines = []
396       for line in contents.splitlines():
397          if line.startswith("allowed_hosts=") and ' ' in line:
398             machines, line = line.split('=', 1)[1].split(' ', 1)
399             if CurrentHost not in machines.split(','):
400                continue # skip this key
401          lines.append(line)
402       if not lines:
403          continue # no keys for this host
404       contents = "\n".join(lines) + "\n"
405       to.size = len(contents)
406       tf.addfile(to, StringIO(contents))
407
408    tf.close()
409    os.rename(os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
410
411 # add a list of groups to existing groups,
412 # including all subgroups thereof, recursively.
413 # basically this proceduces the transitive hull of the groups in
414 # addgroups.
415 def addGroups(existingGroups, newGroups, uid):
416    for group in newGroups:
417       # if it's a <group>@host, split it and verify it's on the current host.
418       s = group.split('@', 1)
419       if len(s) == 2 and s[1] != CurrentHost:
420          continue
421       group = s[0]
422
423       # let's see if we handled this group already
424       if group in existingGroups:
425          continue
426
427       if not GroupIDMap.has_key(group):
428          print "Group", group, "does not exist but", uid, "is in it"
429          continue
430
431       existingGroups.append(group)
432
433       if SubGroupMap.has_key(group):
434          addGroups(existingGroups, SubGroupMap[group], uid)
435
436 # Generate the group list
437 def GenGroup(File):
438    grouprevmap = {}
439    F = None
440    try:
441       F = open(File + ".tdb.tmp", "w")
442      
443       # Generate the GroupMap
444       GroupMap = {}
445       for x in GroupIDMap.keys():
446          GroupMap[x] = []
447       GroupHasPrimaryMembers = {}
448      
449       # Fetch all the users
450       global PasswdAttrs
451      
452       # Sort them into a list of groups having a set of users
453       for x in PasswdAttrs:
454          a = UDLdap.Account(x[0], x[1])
455          GroupHasPrimaryMembers[ a['gidNumber'] ] = True
456          if not IsInGroup(x): continue
457          if not 'supplementaryGid' in a: continue
458
459          supgroups=[]
460          addGroups(supgroups, a['supplementaryGid'], a['uid'])
461          for g in supgroups:
462             GroupMap[g].append(a['uid'])
463
464       # Output the group file.
465       J = 0
466       for x in GroupMap.keys():
467          if GroupIDMap.has_key(x) == 0:
468             continue
469
470          if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
471             continue
472
473          grouprevmap[GroupIDMap[x]] = x
474
475          Line = "%s:x:%u:" % (x, GroupIDMap[x])
476          Comma = ''
477          for I in GroupMap[x]:
478             Line = Line + ("%s%s" % (Comma, I))
479             Comma = ','
480          Line = Sanitize(Line) + "\n"
481          F.write("0%u %s" % (J, Line))
482          F.write(".%s %s" % (x, Line))
483          F.write("=%u %s" % (GroupIDMap[x], Line))
484          J = J + 1
485   
486    # Oops, something unspeakable happened.
487    except:
488       Die(File, None, F)
489       raise
490    Done(File, None, F)
491   
492    return grouprevmap
493
494 def CheckForward():
495    global PasswdAttrs
496    for x in PasswdAttrs:
497       if x[1].has_key("emailForward") == 0:
498          continue
499    
500       if not IsInGroup(x):
501          x[1].pop("emailForward")
502          continue
503
504       # Do not allow people to try to buffer overflow busted parsers
505       if len(GetAttr(x, "emailForward")) > 200:
506          x[1].pop("emailForward")
507          continue
508
509       # Check the forwarding address
510       if EmailCheck.match(GetAttr(x, "emailForward")) == None:
511          x[1].pop("emailForward")
512
513 # Generate the email forwarding list
514 def GenForward(File):
515    F = None
516    try:
517       OldMask = os.umask(0022)
518       F = open(File + ".tmp", "w", 0644)
519       os.umask(OldMask)
520      
521       # Fetch all the users
522       global PasswdAttrs
523      
524       # Write out the email address for each user
525       for x in PasswdAttrs:
526          a = UDLdap.Account(x[0], x[1])
527          if not 'emailForward' in a: continue
528          Line = "%s: %s" % (a['uid'], a['emailForward'])
529          Line = Sanitize(Line) + "\n"
530          F.write(Line)
531   
532    # Oops, something unspeakable happened.
533    except:
534       Die(File, F, None)
535       raise
536    Done(File, F, None)
537
538 def GenCDB(File, Users, key):
539    Fdb = None
540    try:
541       OldMask = os.umask(0022)
542       Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
543       os.umask(OldMask)
544
545       # Write out the email address for each user
546       for x in Users:
547          a = UDLdap.Account(x[0], x[1])
548          if not key in a: continue
549          value = a[key]
550          user = a['uid']
551          Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
552
553       Fdb.write("\n")
554    # Oops, something unspeakable happened.
555    except:
556       Fdb.close()
557       raise
558    if Fdb.close() != None:
559       raise "cdbmake gave an error"
560
561 # Generate the anon XEarth marker file
562 def GenMarkers(File):
563    F = None
564    try:
565       F = open(File + ".tmp", "w")
566      
567       # Fetch all the users
568       global PasswdAttrs
569      
570       # Write out the position for each user
571       for x in PasswdAttrs:
572          a = UDLdap.Account(x[0], x[1])
573          if not ('latitude' in a and 'longitude' in a): continue
574          try:
575             Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
576             Line = Sanitize(Line) + "\n"
577             F.write(Line)
578          except:
579             pass
580   
581    # Oops, something unspeakable happened.
582    except:
583       Die(File, F, None)
584       raise
585    Done(File, F, None)
586
587 # Generate the debian-private subscription list
588 def GenPrivate(File):
589    F = None
590    try:
591       F = open(File + ".tmp", "w")
592      
593       # Fetch all the users
594       global DebianDDUsers
595      
596       # Write out the position for each user
597       for x in DebianDDUsers:
598          a = UDLdap.Account(x[0], x[1])
599          if not a.is_active_user(): continue
600          if not 'privateSub' in a: continue
601          try:
602             Line = "%s"%(a['privateSub'])
603             Line = Sanitize(Line) + "\n"
604             F.write(Line)
605          except:
606             pass
607   
608    # Oops, something unspeakable happened.
609    except:
610       Die(File, F, None)
611       raise
612    Done(File, F, None)
613
614 # Generate a list of locked accounts
615 def GenDisabledAccounts(File):
616    F = None
617    try:
618       F = open(File + ".tmp", "w")
619      
620       # Fetch all the users
621       global PasswdAttrs
622       global DisabledUsers
623      
624       I = 0
625       for x in PasswdAttrs:
626          a = UDLdap.Account(x[0], x[1])
627          if a.pw_active(): continue
628          Line = "%s:%s" % (a['uid'], "Account is locked")
629          DisabledUsers.append(x)
630          F.write(Sanitize(Line) + "\n")
631
632    # Oops, something unspeakable happened.
633    except:
634       Die(File, F, None)
635       raise
636    Done(File, F, None)
637
638 # Generate the list of local addresses that refuse all mail
639 def GenMailDisable(File):
640    F = None
641    try:
642       F = open(File + ".tmp", "w")
643      
644       # Fetch all the users
645       global PasswdAttrs
646      
647       for x in PasswdAttrs:
648          a = UDLdap.Account(x[0], x[1])
649          if not 'mailDisableMessage' in a: continue
650          Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
651          Line = Sanitize(Line) + "\n"
652          F.write(Line)
653   
654    # Oops, something unspeakable happened.
655    except:
656       Die(File, F, None)
657       raise
658    Done(File, F, None)
659
660 # Generate a list of uids that should have boolean affects applied
661 def GenMailBool(File, key):
662    F = None
663    try:
664       F = open(File + ".tmp", "w")
665      
666       # Fetch all the users
667       global PasswdAttrs
668      
669       for x in PasswdAttrs:
670          a = UDLdap.Account(x[0], x[1])
671          if not key in a: continue
672          if not a[key] == 'TRUE': continue
673          Line = "%s"%(a['uid'])
674          Line = Sanitize(Line) + "\n"
675          F.write(Line)
676
677    # Oops, something unspeakable happened.
678    except:
679       Die(File, F, None)
680       raise
681    Done(File, F, None)
682
683 # Generate a list of hosts for RBL or whitelist purposes.
684 def GenMailList(File, key):
685    F = None
686    try:
687       F = open(File + ".tmp", "w")
688      
689       # Fetch all the users
690       global PasswdAttrs
691      
692       if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
693       else:                      validregex = re.compile('^[-\w.]+$')
694
695       for x in PasswdAttrs:
696          a = UDLdap.Account(x[0], x[1])
697          if not key in a: continue
698
699          filtered = filter(lambda z: validregex.match(z), a[key])
700          if len(filtered) == 0: continue
701          if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
702          line = a['uid'] + ': ' + ' : '.join(filtered)
703          line = Sanitize(line) + "\n"
704          F.write(line)
705
706    # Oops, something unspeakable happened.
707    except:
708       Die(File, F, None)
709       raise
710    Done(File, F, None)
711
712 def isRoleAccount(pwEntry):
713    if not pwEntry.has_key("objectClass"):
714       raise "pwEntry has no objectClass"
715    oc =  pwEntry['objectClass']
716    try:
717       i = oc.index('debianRoleAccount')
718       return True
719    except ValueError:
720       return False
721
722 # Generate the DNS Zone file
723 def GenDNS(File):
724    F = None
725    try:
726       F = open(File + ".tmp", "w")
727      
728       # Fetch all the users
729       global PasswdAttrs
730       RRs = {}
731      
732       # Write out the zone file entry for each user
733       for x in PasswdAttrs:
734          if x[1].has_key("dnsZoneEntry") == 0:
735             continue
736      
737          # If the account has no PGP key, do not write it
738          if x[1].has_key("keyFingerPrint") == 0 and not isRoleAccount(x[1]):
739             continue
740          try:
741             F.write("; %s\n"%(EmailAddress(x)))
742             for z in x[1]["dnsZoneEntry"]:
743                Split = z.lower().split()
744                if Split[1].lower() == 'in':
745                   for y in range(0, len(Split)):
746                      if Split[y] == "$":
747                         Split[y] = "\n\t"
748                   Line = " ".join(Split) + "\n"
749                   F.write(Line)
750      
751                   Host = Split[0] + DNSZone
752                   if BSMTPCheck.match(Line) != None:
753                      F.write("; Has BSMTP\n")
754      
755                   # Write some identification information
756                   if not RRs.has_key(Host):
757                      if Split[2].lower() in ["a", "aaaa"]:
758                         Line = "%s IN TXT \"%s\"\n"%(Split[0], EmailAddress(x))
759                         for y in x[1]["keyFingerPrint"]:
760                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
761                            F.write(Line)
762                         RRs[Host] = 1
763                else:
764                   Line = "; Err %s"%(str(Split))
765                   F.write(Line)
766      
767             F.write("\n")
768          except:
769             F.write("; Errors\n")
770             pass
771   
772    # Oops, something unspeakable happened.
773    except:
774       Die(File, F, None)
775       raise
776    Done(File, F, None)
777
778 def ExtractDNSInfo(x):
779
780    TTLprefix="\t"
781    if 'dnsTTL' in x[1]:
782       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
783
784    DNSInfo = []
785    if x[1].has_key("ipHostNumber"):
786       for I in x[1]["ipHostNumber"]:
787          if IsV6Addr.match(I) != None:
788             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
789          else:
790             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
791
792    Algorithm = None
793
794    if 'sshRSAHostKey' in x[1]:
795       for I in x[1]["sshRSAHostKey"]:
796          Split = I.split()
797          if Split[0] == 'ssh-rsa':
798             Algorithm = 1
799          if Split[0] == 'ssh-dss':
800             Algorithm = 2
801          if Algorithm == None:
802             continue
803          Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
804          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
805
806    if 'architecture' in x[1]:
807       Arch = GetAttr(x, "architecture")
808       Mach = ""
809       if x[1].has_key("machine"):
810          Mach = " " + GetAttr(x, "machine")
811       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
812
813    if x[1].has_key("mXRecord"):
814       for I in x[1]["mXRecord"]:
815          DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
816
817    return DNSInfo
818
819 # Generate the DNS records
820 def GenZoneRecords(File):
821    F = None
822    try:
823       F = open(File + ".tmp", "w")
824
825       # Fetch all the hosts
826       global HostAttrs
827
828       for x in HostAttrs:
829          if x[1].has_key("hostname") == 0:
830             continue
831
832          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
833             continue
834
835          DNSInfo = ExtractDNSInfo(x)
836          start = True
837          for Line in DNSInfo:
838             if start == True:
839                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
840                start = False
841             else:
842                Line = "\t\t\t%s" % (Line)
843
844             F.write(Line + "\n")
845
846         # this would write sshfp lines for services on machines
847         # but we can't yet, since some are cnames and we'll make
848         # an invalid zonefile
849         #
850         # for i in x[1].get("purpose", []):
851         #    m = PurposeHostField.match(i)
852         #    if m:
853         #       m = m.group(1)
854         #       # we ignore [[*..]] entries
855         #       if m.startswith('*'):
856         #          continue
857         #       if m.startswith('-'):
858         #          m = m[1:]
859         #       if m:
860         #          if not m.endswith(HostDomain):
861         #             continue
862         #          if not m.endswith('.'):
863         #             m = m + "."
864         #          for Line in DNSInfo:
865         #             if isSSHFP.match(Line):
866         #                Line = "%s\t%s" % (m, Line)
867         #                F.write(Line + "\n")
868
869    # Oops, something unspeakable happened.
870    except:
871       Die(File, F, None)
872       raise
873    Done(File, F, None)
874
875 # Generate the BSMTP file
876 def GenBSMTP(File, HomePrefix):
877    F = None
878    try:
879       F = open(File + ".tmp", "w")
880      
881       # Fetch all the users
882       global PasswdAttrs
883      
884       # Write out the zone file entry for each user
885       for x in PasswdAttrs:
886          if x[1].has_key("dnsZoneEntry") == 0:
887             continue
888      
889          # If the account has no PGP key, do not write it
890          if x[1].has_key("keyFingerPrint") == 0:
891             continue
892          try:
893             for z in x[1]["dnsZoneEntry"]:
894                Split = z.lower().split()
895                if Split[1].lower() == 'in':
896                   for y in range(0, len(Split)):
897                      if Split[y] == "$":
898                         Split[y] = "\n\t"
899                   Line = " ".join(Split) + "\n"
900      
901                   Host = Split[0] + DNSZone
902                   if BSMTPCheck.match(Line) != None:
903                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
904                                   GetAttr(x, "uid"), HomePrefix, GetAttr(x, "uid"), Host))
905      
906          except:
907             F.write("; Errors\n")
908             pass
909   
910    # Oops, something unspeakable happened.
911    except:
912       Die(File, F, None)
913       raise
914    Done(File, F, None)
915   
916 def HostToIP(Host, mapped=True):
917
918    IPAdresses = []
919
920    if Host[1].has_key("ipHostNumber"):
921       for addr in Host[1]["ipHostNumber"]:
922          IPAdresses.append(addr)
923          if IsV6Addr.match(addr) is None and mapped == "True":
924             IPAdresses.append("::ffff:"+addr)
925
926    return IPAdresses
927
928 # Generate the ssh known hosts file
929 def GenSSHKnown(File, mode=None):
930    F = None
931    try:
932       OldMask = os.umask(0022)
933       F = open(File + ".tmp", "w", 0644)
934       os.umask(OldMask)
935      
936       global HostAttrs
937      
938       for x in HostAttrs:
939          if x[1].has_key("hostname") == 0 or \
940             x[1].has_key("sshRSAHostKey") == 0:
941             continue
942          Host = GetAttr(x, "hostname")
943          HostNames = [ Host ]
944          if Host.endswith(HostDomain):
945             HostNames.append(Host[:-(len(HostDomain) + 1)])
946      
947          # in the purpose field [[host|some other text]] (where some other text is optional)
948          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
949          # file.  But so that we don't have to add everything we link we can add an asterisk
950          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
951          # http linking it we also support [[-hostname]] entries.
952          for i in x[1].get("purpose", []):
953             m = PurposeHostField.match(i)
954             if m:
955                m = m.group(1)
956                # we ignore [[*..]] entries
957                if m.startswith('*'):
958                   continue
959                if m.startswith('-'):
960                   m = m[1:]
961                if m:
962                   HostNames.append(m)
963                   if m.endswith(HostDomain):
964                      HostNames.append(m[:-(len(HostDomain) + 1)])
965      
966          for I in x[1]["sshRSAHostKey"]:
967             if mode and mode == 'authorized_keys':
968                hosts = HostToIP(x)
969                if 'sshdistAuthKeysHost' in x[1]:
970                   hosts += x[1]['sshdistAuthKeysHost']
971                Line = 'command="rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (Host, ",".join(hosts), I)
972             else:
973                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
974             Line = Sanitize(Line) + "\n"
975             F.write(Line)
976    # Oops, something unspeakable happened.
977    except:
978       Die(File, F, None)
979       raise
980    Done(File, F, None)
981
982 # Generate the debianhosts file (list of all IP addresses)
983 def GenHosts(File):
984    F = None
985    try:
986       OldMask = os.umask(0022)
987       F = open(File + ".tmp", "w", 0644)
988       os.umask(OldMask)
989      
990       seen = set()
991
992       global HostAttrs
993
994       for x in HostAttrs:
995
996          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
997             continue
998
999          if not 'ipHostNumber' in x[1]:
1000             continue
1001
1002          addrs = x[1]["ipHostNumber"]
1003          for addr in addrs:
1004             if addr not in seen:
1005                seen.add(addr)
1006                addr = Sanitize(addr) + "\n"
1007                F.write(addr)
1008
1009    # Oops, something unspeakable happened.
1010    except:
1011       Die(File, F, None)
1012       raise
1013    Done(File, F, None)
1014
1015 def GenKeyrings(OutDir):
1016    for k in Keyrings:
1017       shutil.copy(k, OutDir)
1018
1019 # Connect to the ldap server
1020 l = connectLDAP()
1021 # for testing purposes it's sometimes useful to pass username/password
1022 # via the environment
1023 if 'UD_CREDENTIALS' in os.environ:
1024    Pass = os.environ['UD_CREDENTIALS'].split()
1025 else:
1026    F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1027    Pass = F.readline().strip().split(" ")
1028    F.close()
1029 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1030
1031 # Fetch all the groups
1032 GroupIDMap = {}
1033 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1034                   ["gid", "gidNumber", "subGroup"])
1035
1036 # Generate the SubGroupMap and GroupIDMap
1037 for x in Attrs:
1038    if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1039       continue
1040    if x[1].has_key("gidNumber") == 0:
1041       continue
1042    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1043    if x[1].has_key("subGroup") != 0:
1044       SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1045
1046 # Fetch all the users
1047 PasswdAttrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0)))",\
1048                 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1049                  "gecos", "loginShell", "userPassword", "shadowLastChange",\
1050                  "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1051                  "shadowExpire", "emailForward", "latitude", "longitude",\
1052                  "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1053                  "keyFingerPrint", "privateSub", "mailDisableMessage",\
1054                  "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1055                  "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1056                  "mailContentInspectionAction"])
1057
1058 if PasswdAttrs is None:
1059    raise UDEmptyList, "No Users"
1060
1061 PasswdAttrs.sort(lambda x, y: cmp((GetAttr(x, "uid")).lower(), (GetAttr(y, "uid")).lower()))
1062
1063 # Fetch all the hosts
1064 HostAttrs    = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1065                 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1066                  "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1067
1068 if HostAttrs == None:
1069    raise UDEmptyList, "No Hosts"
1070
1071 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1072
1073 # override globaldir for testing
1074 if 'UD_GENERATEDIR' in os.environ:
1075    GenerateDir = os.environ['UD_GENERATEDIR']
1076
1077 # Generate global things
1078 GlobalDir = GenerateDir + "/"
1079 GenDisabledAccounts(GlobalDir + "disabled-accounts")
1080
1081 PasswdAttrs = filter(lambda x: not IsRetired(x), PasswdAttrs)
1082 DebianDDUsers = filter(lambda x: IsGidDebian(x), PasswdAttrs)
1083
1084 CheckForward()
1085
1086 GenMailDisable(GlobalDir + "mail-disable")
1087 GenCDB(GlobalDir + "mail-forward.cdb", PasswdAttrs, 'emailForward')
1088 GenCDB(GlobalDir + "mail-contentinspectionaction.cdb", PasswdAttrs, 'mailContentInspectionAction')
1089 GenPrivate(GlobalDir + "debian-private")
1090 GenSSHKnown(GlobalDir+"authorized_keys", 'authorized_keys')
1091 GenMailBool(GlobalDir + "mail-greylist", "mailGreylisting")
1092 GenMailBool(GlobalDir + "mail-callout", "mailCallout")
1093 GenMailList(GlobalDir + "mail-rbl", "mailRBL")
1094 GenMailList(GlobalDir + "mail-rhsbl", "mailRHSBL")
1095 GenMailList(GlobalDir + "mail-whitelist", "mailWhitelist")
1096 GenKeyrings(GlobalDir)
1097
1098 # Compatibility.
1099 GenForward(GlobalDir + "forward-alias")
1100
1101 PasswdAttrs = filter(lambda x: not x in DisabledUsers, PasswdAttrs)
1102
1103 SSHFiles = GenSSHShadow()
1104 GenMarkers(GlobalDir + "markers")
1105 GenSSHKnown(GlobalDir + "ssh_known_hosts")
1106 GenHosts(GlobalDir + "debianhosts")
1107
1108 for host in HostAttrs:
1109    if not "hostname" in host[1]:
1110       continue
1111
1112    CurrentHost = host[1]['hostname'][0]
1113    OutDir = GenerateDir + '/' + CurrentHost + '/'
1114    try:
1115       os.mkdir(OutDir)
1116    except: 
1117       pass
1118
1119    # Get the group list and convert any named groups to numerics
1120    GroupList = {}
1121    for groupname in AllowedGroupsPreload.strip().split(" "):
1122       GroupList[groupname] = True
1123    if 'allowedGroups' in host[1]:
1124       for groupname in host[1]['allowedGroups']:
1125          GroupList[groupname] = True
1126    for groupname in GroupList.keys():
1127       if groupname in GroupIDMap:
1128          GroupList[str(GroupIDMap[groupname])] = True
1129
1130    ExtraList = {}
1131    if 'exportOptions' in host[1]:
1132       for extra in host[1]['exportOptions']:
1133          ExtraList[extra.upper()] = True
1134
1135    Allowed = GroupList
1136    if Allowed == {}:
1137       Allowed = None
1138
1139    DoLink(GlobalDir, OutDir, "debianhosts")
1140    DoLink(GlobalDir, OutDir, "ssh_known_hosts")
1141    DoLink(GlobalDir, OutDir, "disabled-accounts")
1142
1143    sys.stdout.flush()
1144    if 'NOPASSWD' in ExtraList:
1145       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "*")
1146    else:
1147       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "x")
1148    sys.stdout.flush()
1149    grouprevmap = GenGroup(OutDir + "group")
1150    GenShadowSudo(OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1151
1152    # Now we know who we're allowing on the machine, export
1153    # the relevant ssh keys
1154    GenSSHtarballs(userlist, SSHFiles, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1155
1156    if not 'NOPASSWD' in ExtraList:
1157       GenShadow(OutDir + "shadow")
1158
1159    # Link in global things
1160    if not 'NOMARKERS' in ExtraList:
1161       DoLink(GlobalDir, OutDir, "markers")
1162    DoLink(GlobalDir, OutDir, "mail-forward.cdb")
1163    DoLink(GlobalDir, OutDir, "mail-contentinspectionaction.cdb")
1164    DoLink(GlobalDir, OutDir, "mail-disable")
1165    DoLink(GlobalDir, OutDir, "mail-greylist")
1166    DoLink(GlobalDir, OutDir, "mail-callout")
1167    DoLink(GlobalDir, OutDir, "mail-rbl")
1168    DoLink(GlobalDir, OutDir, "mail-rhsbl")
1169    DoLink(GlobalDir, OutDir, "mail-whitelist")
1170    GenCDB(OutDir + "user-forward.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'emailForward')
1171    GenCDB(OutDir + "batv-tokens.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'bATVToken')
1172    GenCDB(OutDir + "default-mail-options.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'mailDefaultOptions')
1173
1174    # Compatibility.
1175    DoLink(GlobalDir, OutDir, "forward-alias")
1176
1177    if 'DNS' in ExtraList:
1178       GenDNS(OutDir + "dns-zone")
1179       GenZoneRecords(OutDir + "dns-sshfp")
1180
1181    if 'AUTHKEYS' in ExtraList:
1182       DoLink(GlobalDir, OutDir, "authorized_keys")
1183
1184    if 'BSMTP' in ExtraList:
1185       GenBSMTP(OutDir + "bsmtp", HomePrefix)
1186
1187    if 'PRIVATE' in ExtraList:
1188       DoLink(GlobalDir, OutDir, "debian-private")
1189
1190    if 'KEYRING' in ExtraList:
1191       for k in Keyrings:
1192         DoLink(GlobalDir, OutDir, os.path.basename(k))
1193    else:
1194       for k in Keyrings:
1195          try: 
1196             posix.remove(OutDir + os.path.basename(k))
1197          except:
1198             pass
1199
1200 # vim:set et:
1201 # vim:set ts=3:
1202 # vim:set shiftwidth=3: