ud-generate: refuse to run as root
[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 try:
35    from cStringIO import StringIO
36 except ImportError:
37    from StringIO import StringIO
38
39 global Allowed
40 global CurrentHost
41
42 if os.getuid() == 0:
43    sys.stderr.write("You should probably not run ud-generate as root.\n")
44    sys.exit(1)
45
46 PasswdAttrs = None
47 DebianUsers = None
48 DisabledUsers = []
49 GroupIDMap = {}
50 SubGroupMap = {}
51 Allowed = None
52 CurrentHost = ""
53
54 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
55
56 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
57 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
58 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
59 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
60 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
61 isSSHFP = re.compile("^\s*IN\s+SSHFP")
62 DNSZone = ".debian.net"
63 Keyrings = ConfModule.sync_keyrings.split(":")
64
65 def safe_makedirs(dir):
66    try:
67       os.makedirs(dir)
68    except OSError, e:
69       if e.errno == errno.EEXIST:
70          pass
71       else:
72          raise e
73
74 def safe_rmtree(dir):
75    try:
76       shutil.rmtree(dir)
77    except OSError, e:
78       if e.errno == errno.ENOENT:
79          pass
80       else:
81          raise e
82
83 def Sanitize(Str):
84    return Str.translate(string.maketrans("\n\r\t", "$$$"))
85
86 def DoLink(From, To, File):
87    try: 
88       posix.remove(To + File)
89    except: 
90       pass
91    posix.link(From + File, To + File)
92
93 def IsRetired(DnRecord):
94    """
95    Looks for accountStatus in the LDAP record and tries to
96    match it against one of the known retired statuses
97    """
98
99    status = GetAttr(DnRecord, "accountStatus", None)
100    if status is None:
101       return False
102
103    line = status.split()
104    status = line[0]
105    
106    if status == "inactive":
107       return True
108
109    elif status == "memorial":
110       return True
111
112    elif status == "retiring":
113       # We'll give them a few extra days over what we said
114       age = 6 * 31 * 24 * 60 * 60
115       try:
116          return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
117       except IndexError:
118          return False
119       except ValueError:
120          return False
121
122    return False
123
124 def IsGidDebian(x):
125    try:
126       return int(GetAttr(x, "gidNumber", 0)) == 800
127    except ValueError:
128       return False
129
130 # See if this user is in the group list
131 def IsInGroup(DnRecord):
132   if Allowed is None:
133      return True
134
135   # See if the primary group is in the list
136   if Allowed.has_key(GetAttr(DnRecord, "gidNumber")) != 0:
137      return True
138
139   # Check the host based ACL
140   if DnRecord[1].has_key("allowedHost") != 0:
141      if CurrentHost in DnRecord[1]["allowedHost"]:
142         return True
143
144   # See if there are supplementary groups
145   if DnRecord[1].has_key("supplementaryGid") == 0:
146      return False
147
148   supgroups=[]
149   addGroups(supgroups, DnRecord[1]["supplementaryGid"], GetAttr(DnRecord, "uid"))
150   for g in supgroups:
151      if Allowed.has_key(g):
152         return True
153   return False
154
155 def Die(File, F, Fdb):
156    if F != None:
157       F.close()
158    if Fdb != None:
159       Fdb.close()
160    try: 
161       os.remove(File + ".tmp")
162    except:
163       pass
164    try: 
165       os.remove(File + ".tdb.tmp")
166    except: 
167       pass
168
169 def Done(File, F, Fdb):
170    if F != None:
171       F.close()
172       os.rename(File + ".tmp", File)
173    if Fdb != None:
174       Fdb.close()
175       os.rename(File + ".tdb.tmp", File + ".tdb")
176
177 # Generate the password list
178 def GenPasswd(File, HomePrefix, PwdMarker):
179    F = None
180    try:
181       F = open(File + ".tdb.tmp", "w")
182      
183       userlist = {}
184       # Fetch all the users
185       global PasswdAttrs
186      
187       I = 0
188       for x in PasswdAttrs:
189          if x[1].has_key("uidNumber") == 0 or not IsInGroup(x):
190             continue
191      
192          # Do not let people try to buffer overflow some busted passwd parser.
193          if len(GetAttr(x, "gecos")) > 100 or len(GetAttr(x, "loginShell")) > 50:
194             continue
195      
196          userlist[GetAttr(x, "uid")] = int(GetAttr(x, "gidNumber"))
197          Line = "%s:%s:%s:%s:%s:%s%s:%s" % (GetAttr(x, "uid"),\
198                  PwdMarker,\
199                  GetAttr(x, "uidNumber"), GetAttr(x, "gidNumber"),\
200                  GetAttr(x, "gecos"), HomePrefix, GetAttr(x, "uid"),\
201                  GetAttr(x, "loginShell"))
202      
203          Line = Sanitize(Line) + "\n"
204          F.write("0%u %s" % (I, Line))
205          F.write(".%s %s" % (GetAttr(x, "uid"), Line))
206          F.write("=%s %s" % (GetAttr(x, "uidNumber"), Line))
207          I = I + 1
208   
209    # Oops, something unspeakable happened.
210    except:
211       Die(File, None, F)
212       raise
213    Done(File, None, F)
214
215    # Return the list of users so we know which keys to export
216    return userlist
217
218 # Generate the shadow list
219 def GenShadow(File):
220    F = None
221    try:
222       OldMask = os.umask(0077)
223       F = open(File + ".tdb.tmp", "w", 0600)
224       os.umask(OldMask)
225      
226       # Fetch all the users
227       global PasswdAttrs
228      
229       I = 0
230       for x in PasswdAttrs:
231          if x[1].has_key("uidNumber") == 0 or not IsInGroup(x):
232             continue
233      
234          Pass = GetAttr(x, "userPassword")
235          if Pass[0:7] != "{crypt}" or len(Pass) > 50:
236             Pass = '*'
237          else:
238             Pass = Pass[7:]
239      
240          # If the account is locked, mark it as such in shadow
241          # See Debian Bug #308229 for why we set it to 1 instead of 0
242          if (GetAttr(x, "userPassword").find("*LK*") != -1) \
243              or GetAttr(x, "userPassword").startswith("!"):
244             ShadowExpire = '1'
245          else:
246             ShadowExpire = GetAttr(x, "shadowExpire")
247      
248          Line = "%s:%s:%s:%s:%s:%s:%s:%s:" % (GetAttr(x, "uid"),\
249                  Pass, GetAttr(x, "shadowLastChange"),\
250                  GetAttr(x, "shadowMin"), GetAttr(x, "shadowMax"),\
251                  GetAttr(x, "shadowWarning"), GetAttr(x, "shadowInactive"),\
252                  ShadowExpire)
253          Line = Sanitize(Line) + "\n"
254          F.write("0%u %s" % (I, Line))
255          F.write(".%s %s" % (GetAttr(x, "uid"), Line))
256          I = I + 1
257   
258    # Oops, something unspeakable happened.
259    except:
260       Die(File, None, F)
261       raise
262    Done(File, None, F)
263
264 # Generate the sudo passwd file
265 def GenShadowSudo(File, untrusted):
266    F = None
267    try:
268       OldMask = os.umask(0077)
269       F = open(File + ".tmp", "w", 0600)
270       os.umask(OldMask)
271      
272       # Fetch all the users
273       global PasswdAttrs
274      
275       for x in PasswdAttrs:
276          Pass = '*'
277          if x[1].has_key("uidNumber") == 0 or not IsInGroup(x):
278             continue
279      
280          if x[1].has_key('sudoPassword'):
281             for entry in x[1]['sudoPassword']:
282                Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
283                if Match == None:
284                   continue
285                uuid = Match.group(1)
286                status = Match.group(2)
287                hosts = Match.group(3)
288                cryptedpass = Match.group(4)
289      
290                if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', x[1]['uid'][0], uuid, hosts, cryptedpass):
291                   continue
292                for_all = hosts == "*"
293                for_this_host = CurrentHost in hosts.split(',')
294                if not (for_all or for_this_host):
295                   continue
296                # ignore * passwords for untrusted hosts, but copy host specific passwords
297                if for_all and untrusted:
298                   continue
299                Pass = cryptedpass
300                if for_this_host: # this makes sure we take a per-host entry over the for-all entry
301                   break
302             if len(Pass) > 50:
303                Pass = '*'
304      
305          Line = "%s:%s" % (GetAttr(x, "uid"), Pass)
306          Line = Sanitize(Line) + "\n"
307          F.write("%s" % (Line))
308   
309    # Oops, something unspeakable happened.
310    except:
311       Die(File, F, None)
312       raise
313    Done(File, F, None)
314
315 # Generate the shadow list
316 def GenSSHShadow():
317    # Fetch all the users
318    userfiles = []
319
320    global PasswdAttrs
321
322    safe_rmtree(os.path.join(GlobalDir, 'userkeys'))
323    safe_makedirs(os.path.join(GlobalDir, 'userkeys'))
324
325    for x in PasswdAttrs:
326
327       if x[1].has_key("uidNumber") == 0 or \
328          x[1].has_key("sshRSAAuthKey") == 0:
329          continue
330
331       User = GetAttr(x, "uid")
332       F = None
333
334       try:
335          OldMask = os.umask(0077)
336          File = os.path.join(GlobalDir, 'userkeys', User)
337          F = open(File + ".tmp", "w", 0600)
338          os.umask(OldMask)
339
340          for I in x[1]["sshRSAAuthKey"]:
341             MultipleLine = "%s" % I
342             MultipleLine = Sanitize(MultipleLine) + "\n"
343             F.write(MultipleLine)
344
345          Done(File, F, None)
346          userfiles.append(os.path.basename(File))
347
348       # Oops, something unspeakable happened.
349       except IOError:
350          Die(File, F, None)
351          # As neither masterFileName nor masterFile are defined at any point
352          # this will raise a NameError.
353          Die(masterFileName, masterFile, None)
354          raise
355
356    return userfiles
357
358 def GenSSHtarballs(userlist, SSHFiles, grouprevmap, target):
359    OldMask = os.umask(0077)
360    tf = tarfile.open(name=os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
361    os.umask(OldMask)
362    for f in userlist.keys():
363       if f not in SSHFiles:
364          continue
365       # If we're not exporting their primary group, don't export
366       # the key and warn
367       grname = None
368       if userlist[f] in grouprevmap.keys():
369          grname = grouprevmap[userlist[f]]
370       else:
371          try:
372             if int(userlist[f]) <= 100:
373                # In these cases, look it up in the normal way so we
374                # deal with cases where, for instance, users are in group
375                # users as their primary group.
376                grname = grp.getgrgid(userlist[f])[0]
377          except Exception, e:
378             pass
379
380       if grname is None:
381          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])
382          continue
383
384       to = tf.gettarinfo(os.path.join(GlobalDir, 'userkeys', f), f)
385       # These will only be used where the username doesn't
386       # exist on the target system for some reason; hence,
387       # in those cases, the safest thing is for the file to
388       # be owned by root but group nobody.  This deals with
389       # the bloody obscure case where the group fails to exist
390       # whilst the user does (in which case we want to avoid
391       # ending up with a file which is owned user:root to avoid
392       # a fairly obvious attack vector)
393       to.uid = 0
394       to.gid = 65534
395       # Using the username / groupname fields avoids any need
396       # to give a shit^W^W^Wcare about the UIDoffset stuff.
397       to.uname = f
398       to.gname = grname
399       to.mode  = 0400
400
401       contents = file(os.path.join(GlobalDir, 'userkeys', f)).read()
402       lines = []
403       for line in contents.splitlines():
404          if line.startswith("allowed_hosts=") and ' ' in line:
405             machines, line = line.split('=', 1)[1].split(' ', 1)
406             if CurrentHost not in machines.split(','):
407                continue # skip this key
408          lines.append(line)
409       if not lines:
410          continue # no keys for this host
411       contents = "\n".join(lines) + "\n"
412       to.size = len(contents)
413       tf.addfile(to, StringIO(contents))
414
415    tf.close()
416    os.rename(os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
417
418 # add a list of groups to existing groups,
419 # including all subgroups thereof, recursively.
420 # basically this proceduces the transitive hull of the groups in
421 # addgroups.
422 def addGroups(existingGroups, newGroups, uid):
423    for group in newGroups:
424       # if it's a <group>@host, split it and verify it's on the current host.
425       s = group.split('@', 1)
426       if len(s) == 2 and s[1] != CurrentHost:
427          continue
428       group = s[0]
429
430       # let's see if we handled this group already
431       if group in existingGroups:
432          continue
433
434       if not GroupIDMap.has_key(group):
435          print "Group", group, "does not exist but", uid, "is in it"
436          continue
437
438       existingGroups.append(group)
439
440       if SubGroupMap.has_key(group):
441          addGroups(existingGroups, SubGroupMap[group], uid)
442
443 # Generate the group list
444 def GenGroup(File):
445    grouprevmap = {}
446    F = None
447    try:
448       F = open(File + ".tdb.tmp", "w")
449      
450       # Generate the GroupMap
451       GroupMap = {}
452       for x in GroupIDMap.keys():
453          GroupMap[x] = []
454       GroupHasPrimaryMembers = {}
455      
456       # Fetch all the users
457       global PasswdAttrs
458      
459       # Sort them into a list of groups having a set of users
460       for x in PasswdAttrs:
461          uid = GetAttr(x, "uid")
462          if 'gidNumber' in x[1]:
463             GroupHasPrimaryMembers[ int(x[1]["gidNumber"][0]) ] = True
464          if x[1].has_key("uidNumber") == 0 or not IsInGroup(x):
465             continue
466          if x[1].has_key("supplementaryGid") == 0:
467             continue
468      
469          supgroups=[]
470          addGroups(supgroups, x[1]["supplementaryGid"], uid)
471          for g in supgroups:
472             GroupMap[g].append(uid)
473      
474       # Output the group file.
475       J = 0
476       for x in GroupMap.keys():
477          if GroupIDMap.has_key(x) == 0:
478             continue
479
480          if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
481             continue
482
483          grouprevmap[GroupIDMap[x]] = x
484
485          Line = "%s:x:%u:" % (x, GroupIDMap[x])
486          Comma = ''
487          for I in GroupMap[x]:
488             Line = Line + ("%s%s" % (Comma, I))
489             Comma = ','
490          Line = Sanitize(Line) + "\n"
491          F.write("0%u %s" % (J, Line))
492          F.write(".%s %s" % (x, Line))
493          F.write("=%u %s" % (GroupIDMap[x], Line))
494          J = J + 1
495   
496    # Oops, something unspeakable happened.
497    except:
498       Die(File, None, F)
499       raise
500    Done(File, None, F)
501   
502    return grouprevmap
503
504 def CheckForward():
505    global PasswdAttrs
506    for x in PasswdAttrs:
507       if x[1].has_key("emailForward") == 0:
508          continue
509    
510       if not IsInGroup(x):
511          x[1].pop("emailForward")
512          continue
513
514       # Do not allow people to try to buffer overflow busted parsers
515       if len(GetAttr(x, "emailForward")) > 200:
516          x[1].pop("emailForward")
517          continue
518
519       # Check the forwarding address
520       if EmailCheck.match(GetAttr(x, "emailForward")) == None:
521          x[1].pop("emailForward")
522
523 # Generate the email forwarding list
524 def GenForward(File):
525    F = None
526    try:
527       OldMask = os.umask(0022)
528       F = open(File + ".tmp", "w", 0644)
529       os.umask(OldMask)
530      
531       # Fetch all the users
532       global PasswdAttrs
533      
534       # Write out the email address for each user
535       for x in PasswdAttrs:
536          if x[1].has_key("emailForward") == 0:
537             continue
538      
539          Line = "%s: %s" % (GetAttr(x, "uid"), GetAttr(x, "emailForward"))
540          Line = Sanitize(Line) + "\n"
541          F.write(Line)
542   
543    # Oops, something unspeakable happened.
544    except:
545       Die(File, F, None)
546       raise
547    Done(File, F, None)
548
549 def GenCDB(File, Users, Key):
550    Fdb = None
551    try:
552       OldMask = os.umask(0022)
553       Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
554       os.umask(OldMask)
555
556       # Write out the email address for each user
557       for x in Users:
558          if not Key in x[1]:
559             continue
560          Value = GetAttr(x, Key)
561          User = GetAttr(x, "uid")
562          Fdb.write("+%d,%d:%s->%s\n" % (len(User), len(Value), User, Value))
563
564       Fdb.write("\n")
565    # Oops, something unspeakable happened.
566    except:
567       Fdb.close()
568       raise
569    if Fdb.close() != None:
570       raise "cdbmake gave an error"
571
572 # Generate the anon XEarth marker file
573 def GenMarkers(File):
574    F = None
575    try:
576       F = open(File + ".tmp", "w")
577      
578       # Fetch all the users
579       global PasswdAttrs
580      
581       # Write out the position for each user
582       for x in PasswdAttrs:
583          if x[1].has_key("latitude") == 0 or x[1].has_key("longitude") == 0:
584             continue
585          try:
586             Line = "%8s %8s \"\""%(DecDegree(GetAttr(x, "latitude"), 1), DecDegree(GetAttr(x, "longitude"), 1))
587             Line = Sanitize(Line) + "\n"
588             F.write(Line)
589          except:
590             pass
591   
592    # Oops, something unspeakable happened.
593    except:
594       Die(File, F, None)
595       raise
596    Done(File, F, None)
597
598 # Generate the debian-private subscription list
599 def GenPrivate(File):
600    F = None
601    try:
602       F = open(File + ".tmp", "w")
603      
604       # Fetch all the users
605       global DebianDDUsers
606      
607       # Write out the position for each user
608       for x in DebianDDUsers:
609          if x[1].has_key("privateSub") == 0:
610             continue
611      
612          # If the account has no PGP key, do not write it
613          if x[1].has_key("keyFingerPrint") == 0:
614             continue
615      
616          try:
617             Line = "%s"%(GetAttr(x, "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 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1080 Pass = F.readline().strip().split(" ")
1081 F.close()
1082 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1083
1084 # Fetch all the groups
1085 GroupIDMap = {}
1086 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1087                   ["gid", "gidNumber", "subGroup"])
1088
1089 # Generate the SubGroupMap and GroupIDMap
1090 for x in Attrs:
1091    if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1092       continue
1093    if x[1].has_key("gidNumber") == 0:
1094       continue
1095    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1096    if x[1].has_key("subGroup") != 0:
1097       SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1098
1099 # Fetch all the users
1100 PasswdAttrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "uid=*",\
1101                 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1102                  "gecos", "loginShell", "userPassword", "shadowLastChange",\
1103                  "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1104                  "shadowExpire", "emailForward", "latitude", "longitude",\
1105                  "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1106                  "keyFingerPrint", "privateSub", "mailDisableMessage",\
1107                  "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1108                  "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1109                  "mailContentInspectionAction"])
1110
1111 if PasswdAttrs is None:
1112    raise UDEmptyList, "No Users"
1113
1114 PasswdAttrs.sort(lambda x, y: cmp((GetAttr(x, "uid")).lower(), (GetAttr(y, "uid")).lower()))
1115
1116 # Fetch all the hosts
1117 HostAttrs    = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1118                 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1119                  "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1120
1121 if HostAttrs == None:
1122    raise UDEmptyList, "No Hosts"
1123
1124 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1125
1126 # Generate global things
1127 GlobalDir = GenerateDir + "/"
1128 GenDisabledAccounts(GlobalDir + "disabled-accounts")
1129
1130 PasswdAttrs = filter(lambda x: not IsRetired(x), PasswdAttrs)
1131 DebianDDUsers = filter(lambda x: IsGidDebian(x), PasswdAttrs)
1132
1133 CheckForward()
1134
1135 GenMailDisable(GlobalDir + "mail-disable")
1136 GenCDB(GlobalDir + "mail-forward.cdb", PasswdAttrs, 'emailForward')
1137 GenCDB(GlobalDir + "mail-contentinspectionaction.cdb", PasswdAttrs, 'mailContentInspectionAction')
1138 GenPrivate(GlobalDir + "debian-private")
1139 GenSSHKnown(GlobalDir+"authorized_keys", 'authorized_keys')
1140 GenMailBool(GlobalDir + "mail-greylist", "mailGreylisting")
1141 GenMailBool(GlobalDir + "mail-callout", "mailCallout")
1142 GenMailList(GlobalDir + "mail-rbl", "mailRBL")
1143 GenMailList(GlobalDir + "mail-rhsbl", "mailRHSBL")
1144 GenMailList(GlobalDir + "mail-whitelist", "mailWhitelist")
1145 GenKeyrings(GlobalDir)
1146
1147 # Compatibility.
1148 GenForward(GlobalDir + "forward-alias")
1149
1150 PasswdAttrs = filter(lambda x: not x in DisabledUsers, PasswdAttrs)
1151
1152 SSHFiles = GenSSHShadow()
1153 GenMarkers(GlobalDir + "markers")
1154 GenSSHKnown(GlobalDir + "ssh_known_hosts")
1155 GenHosts(GlobalDir + "debianhosts")
1156
1157 for host in HostAttrs:
1158    if not "hostname" in host[1]:
1159       continue
1160
1161    CurrentHost = host[1]['hostname'][0]
1162    OutDir = GenerateDir + '/' + CurrentHost + '/'
1163    try:
1164       os.mkdir(OutDir)
1165    except: 
1166       pass
1167
1168    # Get the group list and convert any named groups to numerics
1169    GroupList = {}
1170    for groupname in AllowedGroupsPreload.strip().split(" "):
1171       GroupList[groupname] = True
1172    if 'allowedGroups' in host[1]:
1173       for groupname in host[1]['allowedGroups']:
1174          GroupList[groupname] = True
1175    for groupname in GroupList.keys():
1176       if groupname in GroupIDMap:
1177          GroupList[str(GroupIDMap[groupname])] = True
1178
1179    ExtraList = {}
1180    if 'exportOptions' in host[1]:
1181       for extra in host[1]['exportOptions']:
1182          ExtraList[extra.upper()] = True
1183
1184    Allowed = GroupList
1185    if Allowed == {}:
1186       Allowed = None
1187
1188    DoLink(GlobalDir, OutDir, "debianhosts")
1189    DoLink(GlobalDir, OutDir, "ssh_known_hosts")
1190    DoLink(GlobalDir, OutDir, "disabled-accounts")
1191
1192    sys.stdout.flush()
1193    if 'NOPASSWD' in ExtraList:
1194       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "*")
1195    else:
1196       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "x")
1197    sys.stdout.flush()
1198    grouprevmap = GenGroup(OutDir + "group")
1199    GenShadowSudo(OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1200
1201    # Now we know who we're allowing on the machine, export
1202    # the relevant ssh keys
1203    GenSSHtarballs(userlist, SSHFiles, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1204
1205    if not 'NOPASSWD' in ExtraList:
1206       GenShadow(OutDir + "shadow")
1207
1208    # Link in global things
1209    if not 'NOMARKERS' in ExtraList:
1210       DoLink(GlobalDir, OutDir, "markers")
1211    DoLink(GlobalDir, OutDir, "mail-forward.cdb")
1212    DoLink(GlobalDir, OutDir, "mail-contentinspectionaction.cdb")
1213    DoLink(GlobalDir, OutDir, "mail-disable")
1214    DoLink(GlobalDir, OutDir, "mail-greylist")
1215    DoLink(GlobalDir, OutDir, "mail-callout")
1216    DoLink(GlobalDir, OutDir, "mail-rbl")
1217    DoLink(GlobalDir, OutDir, "mail-rhsbl")
1218    DoLink(GlobalDir, OutDir, "mail-whitelist")
1219    GenCDB(OutDir + "user-forward.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'emailForward')
1220    GenCDB(OutDir + "batv-tokens.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'bATVToken')
1221    GenCDB(OutDir + "default-mail-options.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'mailDefaultOptions')
1222
1223    # Compatibility.
1224    DoLink(GlobalDir, OutDir, "forward-alias")
1225
1226    if 'DNS' in ExtraList:
1227       GenDNS(OutDir + "dns-zone")
1228       GenZoneRecords(OutDir + "dns-sshfp")
1229
1230    if 'AUTHKEYS' in ExtraList:
1231       DoLink(GlobalDir, OutDir, "authorized_keys")
1232
1233    if 'BSMTP' in ExtraList:
1234       GenBSMTP(OutDir + "bsmtp", HomePrefix)
1235
1236    if 'PRIVATE' in ExtraList:
1237       DoLink(GlobalDir, OutDir, "debian-private")
1238
1239    if 'KEYRING' in ExtraList:
1240       for k in Keyrings:
1241         DoLink(GlobalDir, OutDir, os.path.basename(k))
1242    else:
1243       for k in Keyrings:
1244          try: 
1245             posix.remove(OutDir + os.path.basename(k))
1246          except:
1247             pass
1248
1249 # vim:set et:
1250 # vim:set ts=3:
1251 # vim:set shiftwidth=3: