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