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