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