Let private generation use Account class
[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          uid = GetAttr(x, "uid")
463          if 'gidNumber' in x[1]:
464             GroupHasPrimaryMembers[ int(x[1]["gidNumber"][0]) ] = True
465          if x[1].has_key("uidNumber") == 0 or not IsInGroup(x):
466             continue
467          if x[1].has_key("supplementaryGid") == 0:
468             continue
469      
470          supgroups=[]
471          addGroups(supgroups, x[1]["supplementaryGid"], uid)
472          for g in supgroups:
473             GroupMap[g].append(uid)
474      
475       # Output the group file.
476       J = 0
477       for x in GroupMap.keys():
478          if GroupIDMap.has_key(x) == 0:
479             continue
480
481          if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
482             continue
483
484          grouprevmap[GroupIDMap[x]] = x
485
486          Line = "%s:x:%u:" % (x, GroupIDMap[x])
487          Comma = ''
488          for I in GroupMap[x]:
489             Line = Line + ("%s%s" % (Comma, I))
490             Comma = ','
491          Line = Sanitize(Line) + "\n"
492          F.write("0%u %s" % (J, Line))
493          F.write(".%s %s" % (x, Line))
494          F.write("=%u %s" % (GroupIDMap[x], Line))
495          J = J + 1
496   
497    # Oops, something unspeakable happened.
498    except:
499       Die(File, None, F)
500       raise
501    Done(File, None, F)
502   
503    return grouprevmap
504
505 def CheckForward():
506    global PasswdAttrs
507    for x in PasswdAttrs:
508       if x[1].has_key("emailForward") == 0:
509          continue
510    
511       if not IsInGroup(x):
512          x[1].pop("emailForward")
513          continue
514
515       # Do not allow people to try to buffer overflow busted parsers
516       if len(GetAttr(x, "emailForward")) > 200:
517          x[1].pop("emailForward")
518          continue
519
520       # Check the forwarding address
521       if EmailCheck.match(GetAttr(x, "emailForward")) == None:
522          x[1].pop("emailForward")
523
524 # Generate the email forwarding list
525 def GenForward(File):
526    F = None
527    try:
528       OldMask = os.umask(0022)
529       F = open(File + ".tmp", "w", 0644)
530       os.umask(OldMask)
531      
532       # Fetch all the users
533       global PasswdAttrs
534      
535       # Write out the email address for each user
536       for x in PasswdAttrs:
537          if x[1].has_key("emailForward") == 0:
538             continue
539      
540          Line = "%s: %s" % (GetAttr(x, "uid"), GetAttr(x, "emailForward"))
541          Line = Sanitize(Line) + "\n"
542          F.write(Line)
543   
544    # Oops, something unspeakable happened.
545    except:
546       Die(File, F, None)
547       raise
548    Done(File, F, None)
549
550 def GenCDB(File, Users, Key):
551    Fdb = None
552    try:
553       OldMask = os.umask(0022)
554       Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
555       os.umask(OldMask)
556
557       # Write out the email address for each user
558       for x in Users:
559          if not Key in x[1]:
560             continue
561          Value = GetAttr(x, Key)
562          User = GetAttr(x, "uid")
563          Fdb.write("+%d,%d:%s->%s\n" % (len(User), len(Value), User, Value))
564
565       Fdb.write("\n")
566    # Oops, something unspeakable happened.
567    except:
568       Fdb.close()
569       raise
570    if Fdb.close() != None:
571       raise "cdbmake gave an error"
572
573 # Generate the anon XEarth marker file
574 def GenMarkers(File):
575    F = None
576    try:
577       F = open(File + ".tmp", "w")
578      
579       # Fetch all the users
580       global PasswdAttrs
581      
582       # Write out the position for each user
583       for x in PasswdAttrs:
584          a = UDLdap.Account(x[0], x[1])
585          if not ('latitude' in a and 'longitude' in a):
586             continue
587          try:
588             Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
589             Line = Sanitize(Line) + "\n"
590             F.write(Line)
591          except:
592             pass
593   
594    # Oops, something unspeakable happened.
595    except:
596       Die(File, F, None)
597       raise
598    Done(File, F, None)
599
600 # Generate the debian-private subscription list
601 def GenPrivate(File):
602    F = None
603    try:
604       F = open(File + ".tmp", "w")
605      
606       # Fetch all the users
607       global DebianDDUsers
608      
609       # Write out the position for each user
610       for x in DebianDDUsers:
611          a = UDLdap.Account(x[0], x[1])
612          if not a.is_active_user():
613             continue
614          if not 'privateSub' in a:
615             continue
616          try:
617             Line = "%s"%(a['privateSub'])
618             Line = Sanitize(Line) + "\n"
619             F.write(Line)
620          except:
621             pass
622   
623    # Oops, something unspeakable happened.
624    except:
625       Die(File, F, None)
626       raise
627    Done(File, F, None)
628
629 # Generate a list of locked accounts
630 def GenDisabledAccounts(File):
631    F = None
632    try:
633       F = open(File + ".tmp", "w")
634      
635       # Fetch all the users
636       global PasswdAttrs
637       global DisabledUsers
638      
639       I = 0
640       for x in PasswdAttrs:
641          if x[1].has_key("uidNumber") == 0:
642             continue
643      
644          Pass = GetAttr(x, "userPassword")
645          Line = ""
646          # *LK* is the reference value for a locked account
647          # password starting with ! is also a locked account
648          if Pass.find("*LK*") != -1 or Pass.startswith("!"):
649             # Format is <login>:<reason>
650             Line = "%s:%s" % (GetAttr(x, "uid"), "Account is locked")
651             DisabledUsers.append(x)
652      
653          if Line != "":
654             F.write(Sanitize(Line) + "\n")
655      
656    
657    # Oops, something unspeakable happened.
658    except:
659       Die(File, F, None)
660       raise
661    Done(File, F, None)
662
663 # Generate the list of local addresses that refuse all mail
664 def GenMailDisable(File):
665    F = None
666    try:
667       F = open(File + ".tmp", "w")
668      
669       # Fetch all the users
670       global PasswdAttrs
671      
672       for x in PasswdAttrs:
673          Reason = None
674      
675          if x[1].has_key("mailDisableMessage"):
676             Reason = GetAttr(x, "mailDisableMessage")
677          else:
678             continue
679      
680          try:
681             Line = "%s: %s"%(GetAttr(x, "uid"), Reason)
682             Line = Sanitize(Line) + "\n"
683             F.write(Line)
684          except:
685             pass
686   
687    # Oops, something unspeakable happened.
688    except:
689       Die(File, F, None)
690       raise
691    Done(File, F, None)
692
693 # Generate a list of uids that should have boolean affects applied
694 def GenMailBool(File, Key):
695    F = None
696    try:
697       F = open(File + ".tmp", "w")
698      
699       # Fetch all the users
700       global PasswdAttrs
701      
702       for x in PasswdAttrs:
703          Reason = None
704      
705          if x[1].has_key(Key) == 0:
706             continue
707      
708          if GetAttr(x, Key) != "TRUE":
709             continue
710      
711          try:
712             Line = "%s"%(GetAttr(x, "uid"))
713             Line = Sanitize(Line) + "\n"
714             F.write(Line)
715          except:
716             pass
717   
718    # Oops, something unspeakable happened.
719    except:
720       Die(File, F, None)
721       raise
722    Done(File, F, None)
723
724 # Generate a list of hosts for RBL or whitelist purposes.
725 def GenMailList(File, Key):
726    F = None
727    try:
728       F = open(File + ".tmp", "w")
729      
730       # Fetch all the users
731       global PasswdAttrs
732      
733       for x in PasswdAttrs:
734          Reason = None
735      
736          if x[1].has_key(Key) == 0:
737             continue
738      
739          try:
740             found = 0
741             Line = None
742             for z in x[1][Key]:
743                 if Key == "mailWhitelist":
744                    if re.match('^[-\w.]+(/[\d]+)?$', z) == None:
745                       continue
746                 else:
747                    if re.match('^[-\w.]+$', z) == None:
748                       continue
749                 if found == 0:
750                    found = 1
751                    Line = GetAttr(x, "uid")
752                 else:
753                     Line += " "
754                 Line += ": " + z
755                 if Key == "mailRHSBL":
756                    Line += "/$sender_address_domain"
757      
758             if Line != None:
759                Line = Sanitize(Line) + "\n"
760                F.write(Line)
761          except:
762             pass
763   
764    # Oops, something unspeakable happened.
765    except:
766       Die(File, F, None)
767       raise
768    Done(File, F, None)
769
770 def isRoleAccount(pwEntry):
771    if not pwEntry.has_key("objectClass"):
772       raise "pwEntry has no objectClass"
773    oc =  pwEntry['objectClass']
774    try:
775       i = oc.index('debianRoleAccount')
776       return True
777    except ValueError:
778       return False
779
780 # Generate the DNS Zone file
781 def GenDNS(File):
782    F = None
783    try:
784       F = open(File + ".tmp", "w")
785      
786       # Fetch all the users
787       global PasswdAttrs
788       RRs = {}
789      
790       # Write out the zone file entry for each user
791       for x in PasswdAttrs:
792          if x[1].has_key("dnsZoneEntry") == 0:
793             continue
794      
795          # If the account has no PGP key, do not write it
796          if x[1].has_key("keyFingerPrint") == 0 and not isRoleAccount(x[1]):
797             continue
798          try:
799             F.write("; %s\n"%(EmailAddress(x)))
800             for z in x[1]["dnsZoneEntry"]:
801                Split = z.lower().split()
802                if Split[1].lower() == 'in':
803                   for y in range(0, len(Split)):
804                      if Split[y] == "$":
805                         Split[y] = "\n\t"
806                   Line = " ".join(Split) + "\n"
807                   F.write(Line)
808      
809                   Host = Split[0] + DNSZone
810                   if BSMTPCheck.match(Line) != None:
811                      F.write("; Has BSMTP\n")
812      
813                   # Write some identification information
814                   if not RRs.has_key(Host):
815                      if Split[2].lower() in ["a", "aaaa"]:
816                         Line = "%s IN TXT \"%s\"\n"%(Split[0], EmailAddress(x))
817                         for y in x[1]["keyFingerPrint"]:
818                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
819                            F.write(Line)
820                         RRs[Host] = 1
821                else:
822                   Line = "; Err %s"%(str(Split))
823                   F.write(Line)
824      
825             F.write("\n")
826          except:
827             F.write("; Errors\n")
828             pass
829   
830    # Oops, something unspeakable happened.
831    except:
832       Die(File, F, None)
833       raise
834    Done(File, F, None)
835
836 def ExtractDNSInfo(x):
837
838    TTLprefix="\t"
839    if 'dnsTTL' in x[1]:
840       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
841
842    DNSInfo = []
843    if x[1].has_key("ipHostNumber"):
844       for I in x[1]["ipHostNumber"]:
845          if IsV6Addr.match(I) != None:
846             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
847          else:
848             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
849
850    Algorithm = None
851
852    if 'sshRSAHostKey' in x[1]:
853       for I in x[1]["sshRSAHostKey"]:
854          Split = I.split()
855          if Split[0] == 'ssh-rsa':
856             Algorithm = 1
857          if Split[0] == 'ssh-dss':
858             Algorithm = 2
859          if Algorithm == None:
860             continue
861          Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
862          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
863
864    if 'architecture' in x[1]:
865       Arch = GetAttr(x, "architecture")
866       Mach = ""
867       if x[1].has_key("machine"):
868          Mach = " " + GetAttr(x, "machine")
869       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
870
871    if x[1].has_key("mXRecord"):
872       for I in x[1]["mXRecord"]:
873          DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
874
875    return DNSInfo
876
877 # Generate the DNS records
878 def GenZoneRecords(File):
879    F = None
880    try:
881       F = open(File + ".tmp", "w")
882
883       # Fetch all the hosts
884       global HostAttrs
885
886       for x in HostAttrs:
887          if x[1].has_key("hostname") == 0:
888             continue
889
890          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
891             continue
892
893          DNSInfo = ExtractDNSInfo(x)
894          start = True
895          for Line in DNSInfo:
896             if start == True:
897                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
898                start = False
899             else:
900                Line = "\t\t\t%s" % (Line)
901
902             F.write(Line + "\n")
903
904         # this would write sshfp lines for services on machines
905         # but we can't yet, since some are cnames and we'll make
906         # an invalid zonefile
907         #
908         # for i in x[1].get("purpose", []):
909         #    m = PurposeHostField.match(i)
910         #    if m:
911         #       m = m.group(1)
912         #       # we ignore [[*..]] entries
913         #       if m.startswith('*'):
914         #          continue
915         #       if m.startswith('-'):
916         #          m = m[1:]
917         #       if m:
918         #          if not m.endswith(HostDomain):
919         #             continue
920         #          if not m.endswith('.'):
921         #             m = m + "."
922         #          for Line in DNSInfo:
923         #             if isSSHFP.match(Line):
924         #                Line = "%s\t%s" % (m, Line)
925         #                F.write(Line + "\n")
926
927    # Oops, something unspeakable happened.
928    except:
929       Die(File, F, None)
930       raise
931    Done(File, F, None)
932
933 # Generate the BSMTP file
934 def GenBSMTP(File, HomePrefix):
935    F = None
936    try:
937       F = open(File + ".tmp", "w")
938      
939       # Fetch all the users
940       global PasswdAttrs
941      
942       # Write out the zone file entry for each user
943       for x in PasswdAttrs:
944          if x[1].has_key("dnsZoneEntry") == 0:
945             continue
946      
947          # If the account has no PGP key, do not write it
948          if x[1].has_key("keyFingerPrint") == 0:
949             continue
950          try:
951             for z in x[1]["dnsZoneEntry"]:
952                Split = z.lower().split()
953                if Split[1].lower() == 'in':
954                   for y in range(0, len(Split)):
955                      if Split[y] == "$":
956                         Split[y] = "\n\t"
957                   Line = " ".join(Split) + "\n"
958      
959                   Host = Split[0] + DNSZone
960                   if BSMTPCheck.match(Line) != None:
961                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
962                                   GetAttr(x, "uid"), HomePrefix, GetAttr(x, "uid"), Host))
963      
964          except:
965             F.write("; Errors\n")
966             pass
967   
968    # Oops, something unspeakable happened.
969    except:
970       Die(File, F, None)
971       raise
972    Done(File, F, None)
973   
974 def HostToIP(Host, mapped=True):
975
976    IPAdresses = []
977
978    if Host[1].has_key("ipHostNumber"):
979       for addr in Host[1]["ipHostNumber"]:
980          IPAdresses.append(addr)
981          if IsV6Addr.match(addr) is None and mapped == "True":
982             IPAdresses.append("::ffff:"+addr)
983
984    return IPAdresses
985
986 # Generate the ssh known hosts file
987 def GenSSHKnown(File, mode=None):
988    F = None
989    try:
990       OldMask = os.umask(0022)
991       F = open(File + ".tmp", "w", 0644)
992       os.umask(OldMask)
993      
994       global HostAttrs
995      
996       for x in HostAttrs:
997          if x[1].has_key("hostname") == 0 or \
998             x[1].has_key("sshRSAHostKey") == 0:
999             continue
1000          Host = GetAttr(x, "hostname")
1001          HostNames = [ Host ]
1002          if Host.endswith(HostDomain):
1003             HostNames.append(Host[:-(len(HostDomain) + 1)])
1004      
1005          # in the purpose field [[host|some other text]] (where some other text is optional)
1006          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1007          # file.  But so that we don't have to add everything we link we can add an asterisk
1008          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
1009          # http linking it we also support [[-hostname]] entries.
1010          for i in x[1].get("purpose", []):
1011             m = PurposeHostField.match(i)
1012             if m:
1013                m = m.group(1)
1014                # we ignore [[*..]] entries
1015                if m.startswith('*'):
1016                   continue
1017                if m.startswith('-'):
1018                   m = m[1:]
1019                if m:
1020                   HostNames.append(m)
1021                   if m.endswith(HostDomain):
1022                      HostNames.append(m[:-(len(HostDomain) + 1)])
1023      
1024          for I in x[1]["sshRSAHostKey"]:
1025             if mode and mode == 'authorized_keys':
1026                hosts = HostToIP(x)
1027                if 'sshdistAuthKeysHost' in x[1]:
1028                   hosts += x[1]['sshdistAuthKeysHost']
1029                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)
1030             else:
1031                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1032             Line = Sanitize(Line) + "\n"
1033             F.write(Line)
1034    # Oops, something unspeakable happened.
1035    except:
1036       Die(File, F, None)
1037       raise
1038    Done(File, F, None)
1039
1040 # Generate the debianhosts file (list of all IP addresses)
1041 def GenHosts(File):
1042    F = None
1043    try:
1044       OldMask = os.umask(0022)
1045       F = open(File + ".tmp", "w", 0644)
1046       os.umask(OldMask)
1047      
1048       seen = set()
1049
1050       global HostAttrs
1051
1052       for x in HostAttrs:
1053
1054          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1055             continue
1056
1057          if not 'ipHostNumber' in x[1]:
1058             continue
1059
1060          addrs = x[1]["ipHostNumber"]
1061          for addr in addrs:
1062             if addr not in seen:
1063                seen.add(addr)
1064                addr = Sanitize(addr) + "\n"
1065                F.write(addr)
1066
1067    # Oops, something unspeakable happened.
1068    except:
1069       Die(File, F, None)
1070       raise
1071    Done(File, F, None)
1072
1073 def GenKeyrings(OutDir):
1074    for k in Keyrings:
1075       shutil.copy(k, OutDir)
1076
1077 # Connect to the ldap server
1078 l = connectLDAP()
1079 # for testing purposes it's sometimes useful to pass username/password
1080 # via the environment
1081 if 'UD_CREDENTIALS' in os.environ:
1082    Pass = os.environ['UD_CREDENTIALS'].split()
1083 else:
1084    F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1085    Pass = F.readline().strip().split(" ")
1086    F.close()
1087 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1088
1089 # Fetch all the groups
1090 GroupIDMap = {}
1091 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1092                   ["gid", "gidNumber", "subGroup"])
1093
1094 # Generate the SubGroupMap and GroupIDMap
1095 for x in Attrs:
1096    if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1097       continue
1098    if x[1].has_key("gidNumber") == 0:
1099       continue
1100    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1101    if x[1].has_key("subGroup") != 0:
1102       SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1103
1104 # Fetch all the users
1105 PasswdAttrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "uid=*",\
1106                 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1107                  "gecos", "loginShell", "userPassword", "shadowLastChange",\
1108                  "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1109                  "shadowExpire", "emailForward", "latitude", "longitude",\
1110                  "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1111                  "keyFingerPrint", "privateSub", "mailDisableMessage",\
1112                  "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1113                  "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1114                  "mailContentInspectionAction"])
1115
1116 if PasswdAttrs is None:
1117    raise UDEmptyList, "No Users"
1118
1119 PasswdAttrs.sort(lambda x, y: cmp((GetAttr(x, "uid")).lower(), (GetAttr(y, "uid")).lower()))
1120
1121 # Fetch all the hosts
1122 HostAttrs    = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1123                 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1124                  "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1125
1126 if HostAttrs == None:
1127    raise UDEmptyList, "No Hosts"
1128
1129 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1130
1131 # override globaldir for testing
1132 if 'UD_GENERATEDIR' in os.environ:
1133    GenerateDir = os.environ['UD_GENERATEDIR']
1134
1135 # Generate global things
1136 GlobalDir = GenerateDir + "/"
1137 GenDisabledAccounts(GlobalDir + "disabled-accounts")
1138
1139 PasswdAttrs = filter(lambda x: not IsRetired(x), PasswdAttrs)
1140 DebianDDUsers = filter(lambda x: IsGidDebian(x), PasswdAttrs)
1141
1142 CheckForward()
1143
1144 GenMailDisable(GlobalDir + "mail-disable")
1145 GenCDB(GlobalDir + "mail-forward.cdb", PasswdAttrs, 'emailForward')
1146 GenCDB(GlobalDir + "mail-contentinspectionaction.cdb", PasswdAttrs, 'mailContentInspectionAction')
1147 GenPrivate(GlobalDir + "debian-private")
1148 GenSSHKnown(GlobalDir+"authorized_keys", 'authorized_keys')
1149 GenMailBool(GlobalDir + "mail-greylist", "mailGreylisting")
1150 GenMailBool(GlobalDir + "mail-callout", "mailCallout")
1151 GenMailList(GlobalDir + "mail-rbl", "mailRBL")
1152 GenMailList(GlobalDir + "mail-rhsbl", "mailRHSBL")
1153 GenMailList(GlobalDir + "mail-whitelist", "mailWhitelist")
1154 GenKeyrings(GlobalDir)
1155
1156 # Compatibility.
1157 GenForward(GlobalDir + "forward-alias")
1158
1159 PasswdAttrs = filter(lambda x: not x in DisabledUsers, PasswdAttrs)
1160
1161 SSHFiles = GenSSHShadow()
1162 GenMarkers(GlobalDir + "markers")
1163 GenSSHKnown(GlobalDir + "ssh_known_hosts")
1164 GenHosts(GlobalDir + "debianhosts")
1165
1166 for host in HostAttrs:
1167    if not "hostname" in host[1]:
1168       continue
1169
1170    CurrentHost = host[1]['hostname'][0]
1171    OutDir = GenerateDir + '/' + CurrentHost + '/'
1172    try:
1173       os.mkdir(OutDir)
1174    except: 
1175       pass
1176
1177    # Get the group list and convert any named groups to numerics
1178    GroupList = {}
1179    for groupname in AllowedGroupsPreload.strip().split(" "):
1180       GroupList[groupname] = True
1181    if 'allowedGroups' in host[1]:
1182       for groupname in host[1]['allowedGroups']:
1183          GroupList[groupname] = True
1184    for groupname in GroupList.keys():
1185       if groupname in GroupIDMap:
1186          GroupList[str(GroupIDMap[groupname])] = True
1187
1188    ExtraList = {}
1189    if 'exportOptions' in host[1]:
1190       for extra in host[1]['exportOptions']:
1191          ExtraList[extra.upper()] = True
1192
1193    Allowed = GroupList
1194    if Allowed == {}:
1195       Allowed = None
1196
1197    DoLink(GlobalDir, OutDir, "debianhosts")
1198    DoLink(GlobalDir, OutDir, "ssh_known_hosts")
1199    DoLink(GlobalDir, OutDir, "disabled-accounts")
1200
1201    sys.stdout.flush()
1202    if 'NOPASSWD' in ExtraList:
1203       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "*")
1204    else:
1205       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "x")
1206    sys.stdout.flush()
1207    grouprevmap = GenGroup(OutDir + "group")
1208    GenShadowSudo(OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1209
1210    # Now we know who we're allowing on the machine, export
1211    # the relevant ssh keys
1212    GenSSHtarballs(userlist, SSHFiles, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1213
1214    if not 'NOPASSWD' in ExtraList:
1215       GenShadow(OutDir + "shadow")
1216
1217    # Link in global things
1218    if not 'NOMARKERS' in ExtraList:
1219       DoLink(GlobalDir, OutDir, "markers")
1220    DoLink(GlobalDir, OutDir, "mail-forward.cdb")
1221    DoLink(GlobalDir, OutDir, "mail-contentinspectionaction.cdb")
1222    DoLink(GlobalDir, OutDir, "mail-disable")
1223    DoLink(GlobalDir, OutDir, "mail-greylist")
1224    DoLink(GlobalDir, OutDir, "mail-callout")
1225    DoLink(GlobalDir, OutDir, "mail-rbl")
1226    DoLink(GlobalDir, OutDir, "mail-rhsbl")
1227    DoLink(GlobalDir, OutDir, "mail-whitelist")
1228    GenCDB(OutDir + "user-forward.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'emailForward')
1229    GenCDB(OutDir + "batv-tokens.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'bATVToken')
1230    GenCDB(OutDir + "default-mail-options.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'mailDefaultOptions')
1231
1232    # Compatibility.
1233    DoLink(GlobalDir, OutDir, "forward-alias")
1234
1235    if 'DNS' in ExtraList:
1236       GenDNS(OutDir + "dns-zone")
1237       GenZoneRecords(OutDir + "dns-sshfp")
1238
1239    if 'AUTHKEYS' in ExtraList:
1240       DoLink(GlobalDir, OutDir, "authorized_keys")
1241
1242    if 'BSMTP' in ExtraList:
1243       GenBSMTP(OutDir + "bsmtp", HomePrefix)
1244
1245    if 'PRIVATE' in ExtraList:
1246       DoLink(GlobalDir, OutDir, "debian-private")
1247
1248    if 'KEYRING' in ExtraList:
1249       for k in Keyrings:
1250         DoLink(GlobalDir, OutDir, os.path.basename(k))
1251    else:
1252       for k in Keyrings:
1253          try: 
1254             posix.remove(OutDir + os.path.basename(k))
1255          except:
1256             pass
1257
1258 # vim:set et:
1259 # vim:set ts=3:
1260 # vim:set shiftwidth=3: