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