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