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