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