GenDNS
[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(account):
713    return 'debianRoleAccount' in account['objectClass']
714
715 # Generate the DNS Zone file
716 def GenDNS(File):
717    F = None
718    try:
719       F = open(File + ".tmp", "w")
720      
721       # Fetch all the users
722       global PasswdAttrs
723       RRs = {}
724      
725       # Write out the zone file entry for each user
726       for x in PasswdAttrs:
727          a = UDLdap.Account(x[0], x[1])
728          if not 'dnsZoneEntry' in a: continue
729          if not a.is_active_user() and not isRoleAccount(a): continue
730
731          try:
732             F.write("; %s\n"%(a.email_address()))
733             for z in a["dnsZoneEntry"]:
734                Split = z.lower().split()
735                if Split[1].lower() == 'in':
736                   for y in range(0, len(Split)):
737                      if Split[y] == "$":
738                         Split[y] = "\n\t"
739                   Line = " ".join(Split) + "\n"
740                   F.write(Line)
741      
742                   Host = Split[0] + DNSZone
743                   if BSMTPCheck.match(Line) != None:
744                      F.write("; Has BSMTP\n")
745      
746                   # Write some identification information
747                   if not RRs.has_key(Host):
748                      if Split[2].lower() in ["a", "aaaa"]:
749                         Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
750                         for y in a["keyFingerPrint"]:
751                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
752                            F.write(Line)
753                         RRs[Host] = 1
754                else:
755                   Line = "; Err %s"%(str(Split))
756                   F.write(Line)
757      
758             F.write("\n")
759          except Exception, e:
760             F.write("; Errors:\n")
761             for line in str(e).split("\n"):
762                F.write("; %s\n"%(line))
763             pass
764   
765    # Oops, something unspeakable happened.
766    except:
767       Die(File, F, None)
768       raise
769    Done(File, F, None)
770
771 def ExtractDNSInfo(x):
772
773    TTLprefix="\t"
774    if 'dnsTTL' in x[1]:
775       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
776
777    DNSInfo = []
778    if x[1].has_key("ipHostNumber"):
779       for I in x[1]["ipHostNumber"]:
780          if IsV6Addr.match(I) != None:
781             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
782          else:
783             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
784
785    Algorithm = None
786
787    if 'sshRSAHostKey' in x[1]:
788       for I in x[1]["sshRSAHostKey"]:
789          Split = I.split()
790          if Split[0] == 'ssh-rsa':
791             Algorithm = 1
792          if Split[0] == 'ssh-dss':
793             Algorithm = 2
794          if Algorithm == None:
795             continue
796          Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
797          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
798
799    if 'architecture' in x[1]:
800       Arch = GetAttr(x, "architecture")
801       Mach = ""
802       if x[1].has_key("machine"):
803          Mach = " " + GetAttr(x, "machine")
804       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
805
806    if x[1].has_key("mXRecord"):
807       for I in x[1]["mXRecord"]:
808          DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
809
810    return DNSInfo
811
812 # Generate the DNS records
813 def GenZoneRecords(File):
814    F = None
815    try:
816       F = open(File + ".tmp", "w")
817
818       # Fetch all the hosts
819       global HostAttrs
820
821       for x in HostAttrs:
822          if x[1].has_key("hostname") == 0:
823             continue
824
825          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
826             continue
827
828          DNSInfo = ExtractDNSInfo(x)
829          start = True
830          for Line in DNSInfo:
831             if start == True:
832                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
833                start = False
834             else:
835                Line = "\t\t\t%s" % (Line)
836
837             F.write(Line + "\n")
838
839         # this would write sshfp lines for services on machines
840         # but we can't yet, since some are cnames and we'll make
841         # an invalid zonefile
842         #
843         # for i in x[1].get("purpose", []):
844         #    m = PurposeHostField.match(i)
845         #    if m:
846         #       m = m.group(1)
847         #       # we ignore [[*..]] entries
848         #       if m.startswith('*'):
849         #          continue
850         #       if m.startswith('-'):
851         #          m = m[1:]
852         #       if m:
853         #          if not m.endswith(HostDomain):
854         #             continue
855         #          if not m.endswith('.'):
856         #             m = m + "."
857         #          for Line in DNSInfo:
858         #             if isSSHFP.match(Line):
859         #                Line = "%s\t%s" % (m, Line)
860         #                F.write(Line + "\n")
861
862    # Oops, something unspeakable happened.
863    except:
864       Die(File, F, None)
865       raise
866    Done(File, F, None)
867
868 # Generate the BSMTP file
869 def GenBSMTP(File, HomePrefix):
870    F = None
871    try:
872       F = open(File + ".tmp", "w")
873      
874       # Fetch all the users
875       global PasswdAttrs
876      
877       # Write out the zone file entry for each user
878       for x in PasswdAttrs:
879          if x[1].has_key("dnsZoneEntry") == 0:
880             continue
881      
882          # If the account has no PGP key, do not write it
883          if x[1].has_key("keyFingerPrint") == 0:
884             continue
885          try:
886             for z in x[1]["dnsZoneEntry"]:
887                Split = z.lower().split()
888                if Split[1].lower() == 'in':
889                   for y in range(0, len(Split)):
890                      if Split[y] == "$":
891                         Split[y] = "\n\t"
892                   Line = " ".join(Split) + "\n"
893      
894                   Host = Split[0] + DNSZone
895                   if BSMTPCheck.match(Line) != None:
896                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
897                                   GetAttr(x, "uid"), HomePrefix, GetAttr(x, "uid"), Host))
898      
899          except:
900             F.write("; Errors\n")
901             pass
902   
903    # Oops, something unspeakable happened.
904    except:
905       Die(File, F, None)
906       raise
907    Done(File, F, None)
908   
909 def HostToIP(Host, mapped=True):
910
911    IPAdresses = []
912
913    if Host[1].has_key("ipHostNumber"):
914       for addr in Host[1]["ipHostNumber"]:
915          IPAdresses.append(addr)
916          if IsV6Addr.match(addr) is None and mapped == "True":
917             IPAdresses.append("::ffff:"+addr)
918
919    return IPAdresses
920
921 # Generate the ssh known hosts file
922 def GenSSHKnown(File, mode=None):
923    F = None
924    try:
925       OldMask = os.umask(0022)
926       F = open(File + ".tmp", "w", 0644)
927       os.umask(OldMask)
928      
929       global HostAttrs
930      
931       for x in HostAttrs:
932          if x[1].has_key("hostname") == 0 or \
933             x[1].has_key("sshRSAHostKey") == 0:
934             continue
935          Host = GetAttr(x, "hostname")
936          HostNames = [ Host ]
937          if Host.endswith(HostDomain):
938             HostNames.append(Host[:-(len(HostDomain) + 1)])
939      
940          # in the purpose field [[host|some other text]] (where some other text is optional)
941          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
942          # file.  But so that we don't have to add everything we link we can add an asterisk
943          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
944          # http linking it we also support [[-hostname]] entries.
945          for i in x[1].get("purpose", []):
946             m = PurposeHostField.match(i)
947             if m:
948                m = m.group(1)
949                # we ignore [[*..]] entries
950                if m.startswith('*'):
951                   continue
952                if m.startswith('-'):
953                   m = m[1:]
954                if m:
955                   HostNames.append(m)
956                   if m.endswith(HostDomain):
957                      HostNames.append(m[:-(len(HostDomain) + 1)])
958      
959          for I in x[1]["sshRSAHostKey"]:
960             if mode and mode == 'authorized_keys':
961                hosts = HostToIP(x)
962                if 'sshdistAuthKeysHost' in x[1]:
963                   hosts += x[1]['sshdistAuthKeysHost']
964                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)
965             else:
966                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
967             Line = Sanitize(Line) + "\n"
968             F.write(Line)
969    # Oops, something unspeakable happened.
970    except:
971       Die(File, F, None)
972       raise
973    Done(File, F, None)
974
975 # Generate the debianhosts file (list of all IP addresses)
976 def GenHosts(File):
977    F = None
978    try:
979       OldMask = os.umask(0022)
980       F = open(File + ".tmp", "w", 0644)
981       os.umask(OldMask)
982      
983       seen = set()
984
985       global HostAttrs
986
987       for x in HostAttrs:
988
989          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
990             continue
991
992          if not 'ipHostNumber' in x[1]:
993             continue
994
995          addrs = x[1]["ipHostNumber"]
996          for addr in addrs:
997             if addr not in seen:
998                seen.add(addr)
999                addr = Sanitize(addr) + "\n"
1000                F.write(addr)
1001
1002    # Oops, something unspeakable happened.
1003    except:
1004       Die(File, F, None)
1005       raise
1006    Done(File, F, None)
1007
1008 def GenKeyrings(OutDir):
1009    for k in Keyrings:
1010       shutil.copy(k, OutDir)
1011
1012 # Connect to the ldap server
1013 l = connectLDAP()
1014 # for testing purposes it's sometimes useful to pass username/password
1015 # via the environment
1016 if 'UD_CREDENTIALS' in os.environ:
1017    Pass = os.environ['UD_CREDENTIALS'].split()
1018 else:
1019    F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1020    Pass = F.readline().strip().split(" ")
1021    F.close()
1022 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1023
1024 # Fetch all the groups
1025 GroupIDMap = {}
1026 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1027                   ["gid", "gidNumber", "subGroup"])
1028
1029 # Generate the SubGroupMap and GroupIDMap
1030 for x in Attrs:
1031    if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1032       continue
1033    if x[1].has_key("gidNumber") == 0:
1034       continue
1035    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1036    if x[1].has_key("subGroup") != 0:
1037       SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1038
1039 # Fetch all the users
1040 PasswdAttrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0)))",\
1041                 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1042                  "gecos", "loginShell", "userPassword", "shadowLastChange",\
1043                  "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1044                  "shadowExpire", "emailForward", "latitude", "longitude",\
1045                  "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1046                  "keyFingerPrint", "privateSub", "mailDisableMessage",\
1047                  "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1048                  "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1049                  "mailContentInspectionAction"])
1050
1051 if PasswdAttrs is None:
1052    raise UDEmptyList, "No Users"
1053
1054 PasswdAttrs.sort(lambda x, y: cmp((GetAttr(x, "uid")).lower(), (GetAttr(y, "uid")).lower()))
1055
1056 # Fetch all the hosts
1057 HostAttrs    = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1058                 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1059                  "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1060
1061 if HostAttrs == None:
1062    raise UDEmptyList, "No Hosts"
1063
1064 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1065
1066 # override globaldir for testing
1067 if 'UD_GENERATEDIR' in os.environ:
1068    GenerateDir = os.environ['UD_GENERATEDIR']
1069
1070 # Generate global things
1071 GlobalDir = GenerateDir + "/"
1072 GenDisabledAccounts(GlobalDir + "disabled-accounts")
1073
1074 PasswdAttrs = filter(lambda x: not IsRetired(x), PasswdAttrs)
1075 DebianDDUsers = filter(lambda x: IsGidDebian(x), PasswdAttrs)
1076
1077 CheckForward()
1078
1079 GenMailDisable(GlobalDir + "mail-disable")
1080 GenCDB(GlobalDir + "mail-forward.cdb", PasswdAttrs, 'emailForward')
1081 GenCDB(GlobalDir + "mail-contentinspectionaction.cdb", PasswdAttrs, 'mailContentInspectionAction')
1082 GenPrivate(GlobalDir + "debian-private")
1083 GenSSHKnown(GlobalDir+"authorized_keys", 'authorized_keys')
1084 GenMailBool(GlobalDir + "mail-greylist", "mailGreylisting")
1085 GenMailBool(GlobalDir + "mail-callout", "mailCallout")
1086 GenMailList(GlobalDir + "mail-rbl", "mailRBL")
1087 GenMailList(GlobalDir + "mail-rhsbl", "mailRHSBL")
1088 GenMailList(GlobalDir + "mail-whitelist", "mailWhitelist")
1089 GenKeyrings(GlobalDir)
1090
1091 # Compatibility.
1092 GenForward(GlobalDir + "forward-alias")
1093
1094 PasswdAttrs = filter(lambda x: not x in DisabledUsers, PasswdAttrs)
1095
1096 SSHFiles = GenSSHShadow()
1097 GenMarkers(GlobalDir + "markers")
1098 GenSSHKnown(GlobalDir + "ssh_known_hosts")
1099 GenHosts(GlobalDir + "debianhosts")
1100
1101 for host in HostAttrs:
1102    if not "hostname" in host[1]:
1103       continue
1104
1105    CurrentHost = host[1]['hostname'][0]
1106    OutDir = GenerateDir + '/' + CurrentHost + '/'
1107    try:
1108       os.mkdir(OutDir)
1109    except: 
1110       pass
1111
1112    # Get the group list and convert any named groups to numerics
1113    GroupList = {}
1114    for groupname in AllowedGroupsPreload.strip().split(" "):
1115       GroupList[groupname] = True
1116    if 'allowedGroups' in host[1]:
1117       for groupname in host[1]['allowedGroups']:
1118          GroupList[groupname] = True
1119    for groupname in GroupList.keys():
1120       if groupname in GroupIDMap:
1121          GroupList[str(GroupIDMap[groupname])] = True
1122
1123    ExtraList = {}
1124    if 'exportOptions' in host[1]:
1125       for extra in host[1]['exportOptions']:
1126          ExtraList[extra.upper()] = True
1127
1128    Allowed = GroupList
1129    if Allowed == {}:
1130       Allowed = None
1131
1132    DoLink(GlobalDir, OutDir, "debianhosts")
1133    DoLink(GlobalDir, OutDir, "ssh_known_hosts")
1134    DoLink(GlobalDir, OutDir, "disabled-accounts")
1135
1136    sys.stdout.flush()
1137    if 'NOPASSWD' in ExtraList:
1138       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "*")
1139    else:
1140       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "x")
1141    sys.stdout.flush()
1142    grouprevmap = GenGroup(OutDir + "group")
1143    GenShadowSudo(OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1144
1145    # Now we know who we're allowing on the machine, export
1146    # the relevant ssh keys
1147    GenSSHtarballs(userlist, SSHFiles, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1148
1149    if not 'NOPASSWD' in ExtraList:
1150       GenShadow(OutDir + "shadow")
1151
1152    # Link in global things
1153    if not 'NOMARKERS' in ExtraList:
1154       DoLink(GlobalDir, OutDir, "markers")
1155    DoLink(GlobalDir, OutDir, "mail-forward.cdb")
1156    DoLink(GlobalDir, OutDir, "mail-contentinspectionaction.cdb")
1157    DoLink(GlobalDir, OutDir, "mail-disable")
1158    DoLink(GlobalDir, OutDir, "mail-greylist")
1159    DoLink(GlobalDir, OutDir, "mail-callout")
1160    DoLink(GlobalDir, OutDir, "mail-rbl")
1161    DoLink(GlobalDir, OutDir, "mail-rhsbl")
1162    DoLink(GlobalDir, OutDir, "mail-whitelist")
1163    GenCDB(OutDir + "user-forward.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'emailForward')
1164    GenCDB(OutDir + "batv-tokens.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'bATVToken')
1165    GenCDB(OutDir + "default-mail-options.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'mailDefaultOptions')
1166
1167    # Compatibility.
1168    DoLink(GlobalDir, OutDir, "forward-alias")
1169
1170    if 'DNS' in ExtraList:
1171       GenDNS(OutDir + "dns-zone")
1172       GenZoneRecords(OutDir + "dns-sshfp")
1173
1174    if 'AUTHKEYS' in ExtraList:
1175       DoLink(GlobalDir, OutDir, "authorized_keys")
1176
1177    if 'BSMTP' in ExtraList:
1178       GenBSMTP(OutDir + "bsmtp", HomePrefix)
1179
1180    if 'PRIVATE' in ExtraList:
1181       DoLink(GlobalDir, OutDir, "debian-private")
1182
1183    if 'KEYRING' in ExtraList:
1184       for k in Keyrings:
1185         DoLink(GlobalDir, OutDir, os.path.basename(k))
1186    else:
1187       for k in Keyrings:
1188          try: 
1189             posix.remove(OutDir + os.path.basename(k))
1190          except:
1191             pass
1192
1193 # vim:set et:
1194 # vim:set ts=3:
1195 # vim:set shiftwidth=3: