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