Get rid of global variable PasswdAttrs
[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 DebianUsers = None
48 GroupIDMap = {}
49 SubGroupMap = {}
50 Allowed = None
51 CurrentHost = ""
52
53 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
54
55 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
56 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
57 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
58 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
59 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
60 isSSHFP = re.compile("^\s*IN\s+SSHFP")
61 DNSZone = ".debian.net"
62 Keyrings = ConfModule.sync_keyrings.split(":")
63
64 def safe_makedirs(dir):
65    try:
66       os.makedirs(dir)
67    except OSError, e:
68       if e.errno == errno.EEXIST:
69          pass
70       else:
71          raise e
72
73 def safe_rmtree(dir):
74    try:
75       shutil.rmtree(dir)
76    except OSError, e:
77       if e.errno == errno.ENOENT:
78          pass
79       else:
80          raise e
81
82 def Sanitize(Str):
83    return Str.translate(string.maketrans("\n\r\t", "$$$"))
84
85 def DoLink(From, To, File):
86    try: 
87       posix.remove(To + File)
88    except: 
89       pass
90    posix.link(From + File, To + File)
91
92 def IsRetired(account):
93    """
94    Looks for accountStatus in the LDAP record and tries to
95    match it against one of the known retired statuses
96    """
97
98    status = account['accountStatus']
99
100    line = status.split()
101    status = line[0]
102
103    if status == "inactive":
104       return True
105
106    elif status == "memorial":
107       return True
108
109    elif status == "retiring":
110       # We'll give them a few extra days over what we said
111       age = 6 * 31 * 24 * 60 * 60
112       try:
113          return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
114       except IndexError:
115          return False
116       except ValueError:
117          return False
118
119    return False
120
121 #def IsGidDebian(account):
122 #   return account['gidNumber'] == 800
123
124 # See if this user is in the group list
125 def IsInGroup(account):
126   if Allowed is None:
127      return True
128
129   # See if the primary group is in the list
130   if str(account['gidNumber']) in Allowed: return True
131
132   # Check the host based ACL
133   if 'allowedHost' in account and CurrentHost in account['allowedHost']: return True
134
135   # See if there are supplementary groups
136   if not 'supplementaryGid' in account: return False
137
138   supgroups=[]
139   addGroups(supgroups, account['supplementaryGid'], account['uid'])
140   for g in supgroups:
141      if Allowed.has_key(g):
142         return True
143   return False
144
145 def Die(File, F, Fdb):
146    if F != None:
147       F.close()
148    if Fdb != None:
149       Fdb.close()
150    try: 
151       os.remove(File + ".tmp")
152    except:
153       pass
154    try: 
155       os.remove(File + ".tdb.tmp")
156    except: 
157       pass
158
159 def Done(File, F, Fdb):
160    if F != None:
161       F.close()
162       os.rename(File + ".tmp", File)
163    if Fdb != None:
164       Fdb.close()
165       os.rename(File + ".tdb.tmp", File + ".tdb")
166
167 # Generate the password list
168 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
169    F = None
170    try:
171       F = open(File + ".tdb.tmp", "w")
172
173       userlist = {}
174       i = 0
175       for a in accounts:
176          if not IsInGroup(a): continue
177
178          # Do not let people try to buffer overflow some busted passwd parser.
179          if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
180
181          userlist[a['uid']] = a['gidNumber']
182          line = "%s:%s:%d:%d:%s:%s%s:%s" % (
183                  a['uid'],
184                  PwdMarker,
185                  a['uidNumber'],
186                  a['gidNumber'],
187                  a['gecos'],
188                  HomePrefix, a['uid'],
189                  a['loginShell'])
190          line = Sanitize(line) + "\n"
191          F.write("0%u %s" % (i, line))
192          F.write(".%s %s" % (a['uid'], line))
193          F.write("=%d %s" % (a['uidNumber'], line))
194          i = i + 1
195
196    # Oops, something unspeakable happened.
197    except:
198       Die(File, None, F)
199       raise
200    Done(File, None, F)
201
202    # Return the list of users so we know which keys to export
203    return userlist
204
205 # Generate the shadow list
206 def GenShadow(accounts, File):
207    F = None
208    try:
209       OldMask = os.umask(0077)
210       F = open(File + ".tdb.tmp", "w", 0600)
211       os.umask(OldMask)
212
213       i = 0
214       for a in accounts:
215          Pass = '*'
216          if not IsInGroup(a): continue
217
218          # If the account is locked, mark it as such in shadow
219          # See Debian Bug #308229 for why we set it to 1 instead of 0
220          if not a.pw_active():     ShadowExpire = '1'
221          elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
222          else:                     ShadowExpire = ''
223
224          values = []
225          values.append(a['uid'])
226          values.append(a.get_password())
227          for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
228             if key in a: values.append(a[key])
229             else:        values.append('')
230          values.append(ShadowExpire)
231          line = ':'.join(values)+':'
232          line = Sanitize(line) + "\n"
233          F.write("0%u %s" % (i, line))
234          F.write(".%s %s" % (a['uid'], line))
235          i = i + 1
236
237    # Oops, something unspeakable happened.
238    except:
239       Die(File, None, F)
240       raise
241    Done(File, None, F)
242
243 # Generate the sudo passwd file
244 def GenShadowSudo(accounts, File, untrusted):
245    F = None
246    try:
247       OldMask = os.umask(0077)
248       F = open(File + ".tmp", "w", 0600)
249       os.umask(OldMask)
250
251       for a in accounts:
252          Pass = '*'
253          if not IsInGroup(a): continue
254      
255          if 'sudoPassword' in a:
256             for entry in a['sudoPassword']:
257                Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
258                if Match == None:
259                   continue
260                uuid = Match.group(1)
261                status = Match.group(2)
262                hosts = Match.group(3)
263                cryptedpass = Match.group(4)
264      
265                if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
266                   continue
267                for_all = hosts == "*"
268                for_this_host = CurrentHost in hosts.split(',')
269                if not (for_all or for_this_host):
270                   continue
271                # ignore * passwords for untrusted hosts, but copy host specific passwords
272                if for_all and untrusted:
273                   continue
274                Pass = cryptedpass
275                if for_this_host: # this makes sure we take a per-host entry over the for-all entry
276                   break
277             if len(Pass) > 50:
278                Pass = '*'
279      
280          Line = "%s:%s" % (a['uid'], Pass)
281          Line = Sanitize(Line) + "\n"
282          F.write("%s" % (Line))
283   
284    # Oops, something unspeakable happened.
285    except:
286       Die(File, F, None)
287       raise
288    Done(File, F, None)
289
290 # Generate the shadow list
291 def GenSSHShadow(accounts):
292    # Fetch all the users
293    userfiles = []
294
295    safe_rmtree(os.path.join(GlobalDir, 'userkeys'))
296    safe_makedirs(os.path.join(GlobalDir, 'userkeys'))
297
298    for a in accounts:
299       if not 'sshRSAAuthKey' in a: continue
300
301       F = None
302       try:
303          OldMask = os.umask(0077)
304          File = os.path.join(GlobalDir, 'userkeys', a['uid'])
305          F = open(File + ".tmp", "w", 0600)
306          os.umask(OldMask)
307
308          for I in a['sshRSAAuthKey']:
309             MultipleLine = "%s" % I
310             MultipleLine = Sanitize(MultipleLine) + "\n"
311             F.write(MultipleLine)
312
313          Done(File, F, None)
314          userfiles.append(os.path.basename(File))
315
316       # Oops, something unspeakable happened.
317       except IOError:
318          Die(File, F, None)
319          # As neither masterFileName nor masterFile are defined at any point
320          # this will raise a NameError.
321          Die(masterFileName, masterFile, None)
322          raise
323
324    return userfiles
325
326 def GenSSHtarballs(userlist, SSHFiles, grouprevmap, target):
327    OldMask = os.umask(0077)
328    tf = tarfile.open(name=os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
329    os.umask(OldMask)
330    for f in userlist.keys():
331       if f not in SSHFiles:
332          continue
333       # If we're not exporting their primary group, don't export
334       # the key and warn
335       grname = None
336       if userlist[f] in grouprevmap.keys():
337          grname = grouprevmap[userlist[f]]
338       else:
339          try:
340             if int(userlist[f]) <= 100:
341                # In these cases, look it up in the normal way so we
342                # deal with cases where, for instance, users are in group
343                # users as their primary group.
344                grname = grp.getgrgid(userlist[f])[0]
345          except Exception, e:
346             pass
347
348       if grname is None:
349          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])
350          continue
351
352       to = tf.gettarinfo(os.path.join(GlobalDir, 'userkeys', f), f)
353       # These will only be used where the username doesn't
354       # exist on the target system for some reason; hence,
355       # in those cases, the safest thing is for the file to
356       # be owned by root but group nobody.  This deals with
357       # the bloody obscure case where the group fails to exist
358       # whilst the user does (in which case we want to avoid
359       # ending up with a file which is owned user:root to avoid
360       # a fairly obvious attack vector)
361       to.uid = 0
362       to.gid = 65534
363       # Using the username / groupname fields avoids any need
364       # to give a shit^W^W^Wcare about the UIDoffset stuff.
365       to.uname = f
366       to.gname = grname
367       to.mode  = 0400
368
369       contents = file(os.path.join(GlobalDir, 'userkeys', f)).read()
370       lines = []
371       for line in contents.splitlines():
372          if line.startswith("allowed_hosts=") and ' ' in line:
373             machines, line = line.split('=', 1)[1].split(' ', 1)
374             if CurrentHost not in machines.split(','):
375                continue # skip this key
376          lines.append(line)
377       if not lines:
378          continue # no keys for this host
379       contents = "\n".join(lines) + "\n"
380       to.size = len(contents)
381       tf.addfile(to, StringIO(contents))
382
383    tf.close()
384    os.rename(os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
385
386 # add a list of groups to existing groups,
387 # including all subgroups thereof, recursively.
388 # basically this proceduces the transitive hull of the groups in
389 # addgroups.
390 def addGroups(existingGroups, newGroups, uid):
391    for group in newGroups:
392       # if it's a <group>@host, split it and verify it's on the current host.
393       s = group.split('@', 1)
394       if len(s) == 2 and s[1] != CurrentHost:
395          continue
396       group = s[0]
397
398       # let's see if we handled this group already
399       if group in existingGroups:
400          continue
401
402       if not GroupIDMap.has_key(group):
403          print "Group", group, "does not exist but", uid, "is in it"
404          continue
405
406       existingGroups.append(group)
407
408       if SubGroupMap.has_key(group):
409          addGroups(existingGroups, SubGroupMap[group], uid)
410
411 # Generate the group list
412 def GenGroup(accounts, File):
413    grouprevmap = {}
414    F = None
415    try:
416       F = open(File + ".tdb.tmp", "w")
417      
418       # Generate the GroupMap
419       GroupMap = {}
420       for x in GroupIDMap.keys():
421          GroupMap[x] = []
422       GroupHasPrimaryMembers = {}
423
424       # Sort them into a list of groups having a set of users
425       for a in accounts:
426          GroupHasPrimaryMembers[ a['gidNumber'] ] = True
427          if not IsInGroup(a): continue
428          if not 'supplementaryGid' in a: continue
429
430          supgroups=[]
431          addGroups(supgroups, a['supplementaryGid'], a['uid'])
432          for g in supgroups:
433             GroupMap[g].append(a['uid'])
434
435       # Output the group file.
436       J = 0
437       for x in GroupMap.keys():
438          if GroupIDMap.has_key(x) == 0:
439             continue
440
441          if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
442             continue
443
444          grouprevmap[GroupIDMap[x]] = x
445
446          Line = "%s:x:%u:" % (x, GroupIDMap[x])
447          Comma = ''
448          for I in GroupMap[x]:
449             Line = Line + ("%s%s" % (Comma, I))
450             Comma = ','
451          Line = Sanitize(Line) + "\n"
452          F.write("0%u %s" % (J, Line))
453          F.write(".%s %s" % (x, Line))
454          F.write("=%u %s" % (GroupIDMap[x], Line))
455          J = J + 1
456   
457    # Oops, something unspeakable happened.
458    except:
459       Die(File, None, F)
460       raise
461    Done(File, None, F)
462   
463    return grouprevmap
464
465 def CheckForward(accounts):
466    for a in accounts:
467       if not 'emailForward' in a: continue
468
469
470       delete = False
471
472       if not IsInGroup(a): delete = True
473       # Do not allow people to try to buffer overflow busted parsers
474       elif len(a['emailForward']) > 200: delete = True
475       # Check the forwarding address
476       elif EmailCheck.match(a['emailForward']) is None: delete = True
477
478       if delete:
479          a.delete_mailforward()
480
481 # Generate the email forwarding list
482 def GenForward(accounts, File):
483    F = None
484    try:
485       OldMask = os.umask(0022)
486       F = open(File + ".tmp", "w", 0644)
487       os.umask(OldMask)
488
489       for a in accounts:
490          if not 'emailForward' in a: continue
491          Line = "%s: %s" % (a['uid'], a['emailForward'])
492          Line = Sanitize(Line) + "\n"
493          F.write(Line)
494
495    # Oops, something unspeakable happened.
496    except:
497       Die(File, F, None)
498       raise
499    Done(File, F, None)
500
501 def GenCDB(accounts, File, key):
502    Fdb = None
503    try:
504       OldMask = os.umask(0022)
505       Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
506       os.umask(OldMask)
507
508       # Write out the email address for each user
509       for a in accounts:
510          if not key in a: continue
511          value = a[key]
512          user = a['uid']
513          Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
514
515       Fdb.write("\n")
516    # Oops, something unspeakable happened.
517    except:
518       Fdb.close()
519       raise
520    if Fdb.close() != None:
521       raise "cdbmake gave an error"
522
523 # Generate the anon XEarth marker file
524 def GenMarkers(accounts, File):
525    F = None
526    try:
527       F = open(File + ".tmp", "w")
528
529       # Write out the position for each user
530       for a in accounts:
531          if not ('latitude' in a and 'longitude' in a): continue
532          try:
533             Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
534             Line = Sanitize(Line) + "\n"
535             F.write(Line)
536          except:
537             pass
538   
539    # Oops, something unspeakable happened.
540    except:
541       Die(File, F, None)
542       raise
543    Done(File, F, None)
544
545 # Generate the debian-private subscription list
546 def GenPrivate(accounts, File):
547    F = None
548    try:
549       F = open(File + ".tmp", "w")
550
551       # Write out the position for each user
552       for a in accounts:
553          if not a.is_active_user(): continue
554          if not 'privateSub' in a: continue
555          try:
556             Line = "%s"%(a['privateSub'])
557             Line = Sanitize(Line) + "\n"
558             F.write(Line)
559          except:
560             pass
561   
562    # Oops, something unspeakable happened.
563    except:
564       Die(File, F, None)
565       raise
566    Done(File, F, None)
567
568 # Generate a list of locked accounts
569 def GenDisabledAccounts(accounts, File):
570    F = None
571    try:
572       F = open(File + ".tmp", "w")
573       disabled_accounts = []
574
575       # Fetch all the users
576       for a in accounts:
577          if a.pw_active(): continue
578          Line = "%s:%s" % (a['uid'], "Account is locked")
579          disabled_accounts.append(a)
580          F.write(Sanitize(Line) + "\n")
581
582    # Oops, something unspeakable happened.
583    except:
584       Die(File, F, None)
585       raise
586    Done(File, F, None)
587    return disabled_accounts
588
589 # Generate the list of local addresses that refuse all mail
590 def GenMailDisable(accounts, File):
591    F = None
592    try:
593       F = open(File + ".tmp", "w")
594
595       for a in accounts:
596          if not 'mailDisableMessage' in a: continue
597          Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
598          Line = Sanitize(Line) + "\n"
599          F.write(Line)
600
601    # Oops, something unspeakable happened.
602    except:
603       Die(File, F, None)
604       raise
605    Done(File, F, None)
606
607 # Generate a list of uids that should have boolean affects applied
608 def GenMailBool(accounts, File, key):
609    F = None
610    try:
611       F = open(File + ".tmp", "w")
612
613       for a in accounts:
614          if not key in a: continue
615          if not a[key] == 'TRUE': continue
616          Line = "%s"%(a['uid'])
617          Line = Sanitize(Line) + "\n"
618          F.write(Line)
619
620    # Oops, something unspeakable happened.
621    except:
622       Die(File, F, None)
623       raise
624    Done(File, F, None)
625
626 # Generate a list of hosts for RBL or whitelist purposes.
627 def GenMailList(accounts, File, key):
628    F = None
629    try:
630       F = open(File + ".tmp", "w")
631
632       if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
633       else:                      validregex = re.compile('^[-\w.]+$')
634
635       for a in accounts:
636          if not key in a: continue
637
638          filtered = filter(lambda z: validregex.match(z), a[key])
639          if len(filtered) == 0: continue
640          if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
641          line = a['uid'] + ': ' + ' : '.join(filtered)
642          line = Sanitize(line) + "\n"
643          F.write(line)
644
645    # Oops, something unspeakable happened.
646    except:
647       Die(File, F, None)
648       raise
649    Done(File, F, None)
650
651 def isRoleAccount(account):
652    return 'debianRoleAccount' in account['objectClass']
653
654 # Generate the DNS Zone file
655 def GenDNS(accounts, File):
656    F = None
657    try:
658       F = open(File + ".tmp", "w")
659
660       # Fetch all the users
661       RRs = {}
662
663       # Write out the zone file entry for each user
664       for a in accounts:
665          if not 'dnsZoneEntry' in a: continue
666          if not a.is_active_user() and not isRoleAccount(a): continue
667
668          try:
669             F.write("; %s\n"%(a.email_address()))
670             for z in a["dnsZoneEntry"]:
671                Split = z.lower().split()
672                if Split[1].lower() == 'in':
673                   for y in range(0, len(Split)):
674                      if Split[y] == "$":
675                         Split[y] = "\n\t"
676                   Line = " ".join(Split) + "\n"
677                   F.write(Line)
678      
679                   Host = Split[0] + DNSZone
680                   if BSMTPCheck.match(Line) != None:
681                      F.write("; Has BSMTP\n")
682      
683                   # Write some identification information
684                   if not RRs.has_key(Host):
685                      if Split[2].lower() in ["a", "aaaa"]:
686                         Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
687                         for y in a["keyFingerPrint"]:
688                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
689                            F.write(Line)
690                         RRs[Host] = 1
691                else:
692                   Line = "; Err %s"%(str(Split))
693                   F.write(Line)
694      
695             F.write("\n")
696          except Exception, e:
697             F.write("; Errors:\n")
698             for line in str(e).split("\n"):
699                F.write("; %s\n"%(line))
700             pass
701   
702    # Oops, something unspeakable happened.
703    except:
704       Die(File, F, None)
705       raise
706    Done(File, F, None)
707
708 def ExtractDNSInfo(x):
709
710    TTLprefix="\t"
711    if 'dnsTTL' in x[1]:
712       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
713
714    DNSInfo = []
715    if x[1].has_key("ipHostNumber"):
716       for I in x[1]["ipHostNumber"]:
717          if IsV6Addr.match(I) != None:
718             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
719          else:
720             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
721
722    Algorithm = None
723
724    if 'sshRSAHostKey' in x[1]:
725       for I in x[1]["sshRSAHostKey"]:
726          Split = I.split()
727          if Split[0] == 'ssh-rsa':
728             Algorithm = 1
729          if Split[0] == 'ssh-dss':
730             Algorithm = 2
731          if Algorithm == None:
732             continue
733          Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
734          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
735
736    if 'architecture' in x[1]:
737       Arch = GetAttr(x, "architecture")
738       Mach = ""
739       if x[1].has_key("machine"):
740          Mach = " " + GetAttr(x, "machine")
741       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
742
743    if x[1].has_key("mXRecord"):
744       for I in x[1]["mXRecord"]:
745          DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
746
747    return DNSInfo
748
749 # Generate the DNS records
750 def GenZoneRecords(File):
751    F = None
752    try:
753       F = open(File + ".tmp", "w")
754
755       # Fetch all the hosts
756       global HostAttrs
757
758       for x in HostAttrs:
759          if x[1].has_key("hostname") == 0:
760             continue
761
762          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
763             continue
764
765          DNSInfo = ExtractDNSInfo(x)
766          start = True
767          for Line in DNSInfo:
768             if start == True:
769                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
770                start = False
771             else:
772                Line = "\t\t\t%s" % (Line)
773
774             F.write(Line + "\n")
775
776         # this would write sshfp lines for services on machines
777         # but we can't yet, since some are cnames and we'll make
778         # an invalid zonefile
779         #
780         # for i in x[1].get("purpose", []):
781         #    m = PurposeHostField.match(i)
782         #    if m:
783         #       m = m.group(1)
784         #       # we ignore [[*..]] entries
785         #       if m.startswith('*'):
786         #          continue
787         #       if m.startswith('-'):
788         #          m = m[1:]
789         #       if m:
790         #          if not m.endswith(HostDomain):
791         #             continue
792         #          if not m.endswith('.'):
793         #             m = m + "."
794         #          for Line in DNSInfo:
795         #             if isSSHFP.match(Line):
796         #                Line = "%s\t%s" % (m, Line)
797         #                F.write(Line + "\n")
798
799    # Oops, something unspeakable happened.
800    except:
801       Die(File, F, None)
802       raise
803    Done(File, F, None)
804
805 # Generate the BSMTP file
806 def GenBSMTP(accounts, File, HomePrefix):
807    F = None
808    try:
809       F = open(File + ".tmp", "w")
810      
811       # Write out the zone file entry for each user
812       for a in accounts:
813          if not 'dnsZoneEntry' in a: continue
814          if not a.is_active_user(): continue
815
816          try:
817             for z in a["dnsZoneEntry"]:
818                Split = z.lower().split()
819                if Split[1].lower() == 'in':
820                   for y in range(0, len(Split)):
821                      if Split[y] == "$":
822                         Split[y] = "\n\t"
823                   Line = " ".join(Split) + "\n"
824      
825                   Host = Split[0] + DNSZone
826                   if BSMTPCheck.match(Line) != None:
827                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
828                                   a['uid'], HomePrefix, a['uid'], Host))
829      
830          except:
831             F.write("; Errors\n")
832             pass
833   
834    # Oops, something unspeakable happened.
835    except:
836       Die(File, F, None)
837       raise
838    Done(File, F, None)
839   
840 def HostToIP(Host, mapped=True):
841
842    IPAdresses = []
843
844    if Host[1].has_key("ipHostNumber"):
845       for addr in Host[1]["ipHostNumber"]:
846          IPAdresses.append(addr)
847          if IsV6Addr.match(addr) is None and mapped == "True":
848             IPAdresses.append("::ffff:"+addr)
849
850    return IPAdresses
851
852 # Generate the ssh known hosts file
853 def GenSSHKnown(File, mode=None):
854    F = None
855    try:
856       OldMask = os.umask(0022)
857       F = open(File + ".tmp", "w", 0644)
858       os.umask(OldMask)
859      
860       global HostAttrs
861      
862       for x in HostAttrs:
863          if x[1].has_key("hostname") == 0 or \
864             x[1].has_key("sshRSAHostKey") == 0:
865             continue
866          Host = GetAttr(x, "hostname")
867          HostNames = [ Host ]
868          if Host.endswith(HostDomain):
869             HostNames.append(Host[:-(len(HostDomain) + 1)])
870      
871          # in the purpose field [[host|some other text]] (where some other text is optional)
872          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
873          # file.  But so that we don't have to add everything we link we can add an asterisk
874          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
875          # http linking it we also support [[-hostname]] entries.
876          for i in x[1].get("purpose", []):
877             m = PurposeHostField.match(i)
878             if m:
879                m = m.group(1)
880                # we ignore [[*..]] entries
881                if m.startswith('*'):
882                   continue
883                if m.startswith('-'):
884                   m = m[1:]
885                if m:
886                   HostNames.append(m)
887                   if m.endswith(HostDomain):
888                      HostNames.append(m[:-(len(HostDomain) + 1)])
889      
890          for I in x[1]["sshRSAHostKey"]:
891             if mode and mode == 'authorized_keys':
892                hosts = HostToIP(x)
893                if 'sshdistAuthKeysHost' in x[1]:
894                   hosts += x[1]['sshdistAuthKeysHost']
895                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)
896             else:
897                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
898             Line = Sanitize(Line) + "\n"
899             F.write(Line)
900    # Oops, something unspeakable happened.
901    except:
902       Die(File, F, None)
903       raise
904    Done(File, F, None)
905
906 # Generate the debianhosts file (list of all IP addresses)
907 def GenHosts(File):
908    F = None
909    try:
910       OldMask = os.umask(0022)
911       F = open(File + ".tmp", "w", 0644)
912       os.umask(OldMask)
913      
914       seen = set()
915
916       global HostAttrs
917
918       for x in HostAttrs:
919
920          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
921             continue
922
923          if not 'ipHostNumber' in x[1]:
924             continue
925
926          addrs = x[1]["ipHostNumber"]
927          for addr in addrs:
928             if addr not in seen:
929                seen.add(addr)
930                addr = Sanitize(addr) + "\n"
931                F.write(addr)
932
933    # Oops, something unspeakable happened.
934    except:
935       Die(File, F, None)
936       raise
937    Done(File, F, None)
938
939 def GenKeyrings(OutDir):
940    for k in Keyrings:
941       shutil.copy(k, OutDir)
942
943 # Connect to the ldap server
944 l = connectLDAP()
945 # for testing purposes it's sometimes useful to pass username/password
946 # via the environment
947 if 'UD_CREDENTIALS' in os.environ:
948    Pass = os.environ['UD_CREDENTIALS'].split()
949 else:
950    F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
951    Pass = F.readline().strip().split(" ")
952    F.close()
953 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
954
955 # Fetch all the groups
956 GroupIDMap = {}
957 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
958                   ["gid", "gidNumber", "subGroup"])
959
960 # Generate the SubGroupMap and GroupIDMap
961 for x in Attrs:
962    if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
963       continue
964    if x[1].has_key("gidNumber") == 0:
965       continue
966    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
967    if x[1].has_key("subGroup") != 0:
968       SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
969
970 # Fetch all the users
971 passwd_attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0)))",\
972                 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
973                  "gecos", "loginShell", "userPassword", "shadowLastChange",\
974                  "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
975                  "shadowExpire", "emailForward", "latitude", "longitude",\
976                  "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
977                  "keyFingerPrint", "privateSub", "mailDisableMessage",\
978                  "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
979                  "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
980                  "mailContentInspectionAction"])
981
982 if passwd_attrs is None:
983    raise UDEmptyList, "No Users"
984 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
985 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
986
987 # Fetch all the hosts
988 HostAttrs    = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
989                 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
990                  "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
991
992 if HostAttrs == None:
993    raise UDEmptyList, "No Hosts"
994
995 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
996
997 # override globaldir for testing
998 if 'UD_GENERATEDIR' in os.environ:
999    GenerateDir = os.environ['UD_GENERATEDIR']
1000
1001 # Generate global things
1002 GlobalDir = GenerateDir + "/"
1003 accounts_disabled = GenDisabledAccounts(accounts, GlobalDir + "disabled-accounts")
1004
1005 accounts = filter(lambda x: not IsRetired(x), accounts)
1006 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1007
1008 CheckForward(accounts)
1009
1010 GenMailDisable(accounts, GlobalDir + "mail-disable")
1011 GenCDB(accounts, GlobalDir + "mail-forward.cdb", 'emailForward')
1012 GenCDB(accounts, GlobalDir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1013 GenPrivate(accounts, GlobalDir + "debian-private")
1014 GenSSHKnown(GlobalDir+"authorized_keys", 'authorized_keys')
1015 GenMailBool(accounts, GlobalDir + "mail-greylist", "mailGreylisting")
1016 GenMailBool(accounts, GlobalDir + "mail-callout", "mailCallout")
1017 GenMailList(accounts, GlobalDir + "mail-rbl", "mailRBL")
1018 GenMailList(accounts, GlobalDir + "mail-rhsbl", "mailRHSBL")
1019 GenMailList(accounts, GlobalDir + "mail-whitelist", "mailWhitelist")
1020 GenKeyrings(GlobalDir)
1021
1022 # Compatibility.
1023 GenForward(accounts, GlobalDir + "forward-alias")
1024
1025 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1026
1027 SSHFiles = GenSSHShadow(accounts)
1028 GenMarkers(accounts, GlobalDir + "markers")
1029 GenSSHKnown(GlobalDir + "ssh_known_hosts")
1030 GenHosts(GlobalDir + "debianhosts")
1031
1032 for host in HostAttrs:
1033    if not "hostname" in host[1]:
1034       continue
1035
1036    CurrentHost = host[1]['hostname'][0]
1037    OutDir = GenerateDir + '/' + CurrentHost + '/'
1038    try:
1039       os.mkdir(OutDir)
1040    except: 
1041       pass
1042
1043    # Get the group list and convert any named groups to numerics
1044    GroupList = {}
1045    for groupname in AllowedGroupsPreload.strip().split(" "):
1046       GroupList[groupname] = True
1047    if 'allowedGroups' in host[1]:
1048       for groupname in host[1]['allowedGroups']:
1049          GroupList[groupname] = True
1050    for groupname in GroupList.keys():
1051       if groupname in GroupIDMap:
1052          GroupList[str(GroupIDMap[groupname])] = True
1053
1054    ExtraList = {}
1055    if 'exportOptions' in host[1]:
1056       for extra in host[1]['exportOptions']:
1057          ExtraList[extra.upper()] = True
1058
1059    Allowed = GroupList
1060    if Allowed == {}:
1061       Allowed = None
1062
1063    DoLink(GlobalDir, OutDir, "debianhosts")
1064    DoLink(GlobalDir, OutDir, "ssh_known_hosts")
1065    DoLink(GlobalDir, OutDir, "disabled-accounts")
1066
1067    sys.stdout.flush()
1068    if 'NOPASSWD' in ExtraList:
1069       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1070    else:
1071       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1072    sys.stdout.flush()
1073    grouprevmap = GenGroup(accounts, OutDir + "group")
1074    GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1075
1076    # Now we know who we're allowing on the machine, export
1077    # the relevant ssh keys
1078    GenSSHtarballs(userlist, SSHFiles, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1079
1080    if not 'NOPASSWD' in ExtraList:
1081       GenShadow(accounts, OutDir + "shadow")
1082
1083    # Link in global things
1084    if not 'NOMARKERS' in ExtraList:
1085       DoLink(GlobalDir, OutDir, "markers")
1086    DoLink(GlobalDir, OutDir, "mail-forward.cdb")
1087    DoLink(GlobalDir, OutDir, "mail-contentinspectionaction.cdb")
1088    DoLink(GlobalDir, OutDir, "mail-disable")
1089    DoLink(GlobalDir, OutDir, "mail-greylist")
1090    DoLink(GlobalDir, OutDir, "mail-callout")
1091    DoLink(GlobalDir, OutDir, "mail-rbl")
1092    DoLink(GlobalDir, OutDir, "mail-rhsbl")
1093    DoLink(GlobalDir, OutDir, "mail-whitelist")
1094    GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "user-forward.cdb", 'emailForward')
1095    GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "batv-tokens.cdb", 'bATVToken')
1096    GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1097
1098    # Compatibility.
1099    DoLink(GlobalDir, OutDir, "forward-alias")
1100
1101    if 'DNS' in ExtraList:
1102       GenDNS(accounts, OutDir + "dns-zone")
1103       GenZoneRecords(OutDir + "dns-sshfp")
1104
1105    if 'AUTHKEYS' in ExtraList:
1106       DoLink(GlobalDir, OutDir, "authorized_keys")
1107
1108    if 'BSMTP' in ExtraList:
1109       GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1110
1111    if 'PRIVATE' in ExtraList:
1112       DoLink(GlobalDir, OutDir, "debian-private")
1113
1114    if 'KEYRING' in ExtraList:
1115       for k in Keyrings:
1116         DoLink(GlobalDir, OutDir, os.path.basename(k))
1117    else:
1118       for k in Keyrings:
1119          try: 
1120             posix.remove(OutDir + os.path.basename(k))
1121          except:
1122             pass
1123
1124 # vim:set et:
1125 # vim:set ts=3:
1126 # vim:set shiftwidth=3: