Merge from torproject.org:
[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                   for y in range(0, len(Split)):
776                      if Split[y] == "$":
777                         Split[y] = "\n\t"
778                   Line = " ".join(Split) + "\n"
779                   F.write(Line)
780      
781                   Host = Split[0] + DNSZone
782                   if BSMTPCheck.match(Line) != None:
783                      F.write("; Has BSMTP\n")
784      
785                   # Write some identification information
786                   if not RRs.has_key(Host):
787                      if Split[2].lower() in ["a", "aaaa"]:
788                         Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
789                         for y in a["keyFingerPrint"]:
790                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
791                            F.write(Line)
792                         RRs[Host] = 1
793                else:
794                   Line = "; Err %s"%(str(Split))
795                   F.write(Line)
796      
797             F.write("\n")
798          except Exception, e:
799             F.write("; Errors:\n")
800             for line in str(e).split("\n"):
801                F.write("; %s\n"%(line))
802             pass
803   
804    # Oops, something unspeakable happened.
805    except:
806       Die(File, F, None)
807       raise
808    Done(File, F, None)
809
810 def ExtractDNSInfo(x):
811
812    TTLprefix="\t"
813    if 'dnsTTL' in x[1]:
814       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
815
816    DNSInfo = []
817    if x[1].has_key("ipHostNumber"):
818       for I in x[1]["ipHostNumber"]:
819          if IsV6Addr.match(I) != None:
820             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
821          else:
822             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
823
824    Algorithm = None
825
826    if 'sshRSAHostKey' in x[1]:
827       for I in x[1]["sshRSAHostKey"]:
828          Split = I.split()
829          if Split[0] == 'ssh-rsa':
830             Algorithm = 1
831          if Split[0] == 'ssh-dss':
832             Algorithm = 2
833          if Algorithm == None:
834             continue
835          Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
836          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
837
838    if 'architecture' in x[1]:
839       Arch = GetAttr(x, "architecture")
840       Mach = ""
841       if x[1].has_key("machine"):
842          Mach = " " + GetAttr(x, "machine")
843       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
844
845    if x[1].has_key("mXRecord"):
846       for I in x[1]["mXRecord"]:
847          DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
848
849    return DNSInfo
850
851 # Generate the DNS records
852 def GenZoneRecords(host_attrs, File):
853    F = None
854    try:
855       F = open(File + ".tmp", "w")
856
857       # Fetch all the hosts
858       for x in host_attrs:
859          if x[1].has_key("hostname") == 0:
860             continue
861
862          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
863             continue
864
865          DNSInfo = ExtractDNSInfo(x)
866          start = True
867          for Line in DNSInfo:
868             if start == True:
869                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
870                start = False
871             else:
872                Line = "\t\t\t%s" % (Line)
873
874             F.write(Line + "\n")
875
876         # this would write sshfp lines for services on machines
877         # but we can't yet, since some are cnames and we'll make
878         # an invalid zonefile
879         #
880         # for i in x[1].get("purpose", []):
881         #    m = PurposeHostField.match(i)
882         #    if m:
883         #       m = m.group(1)
884         #       # we ignore [[*..]] entries
885         #       if m.startswith('*'):
886         #          continue
887         #       if m.startswith('-'):
888         #          m = m[1:]
889         #       if m:
890         #          if not m.endswith(HostDomain):
891         #             continue
892         #          if not m.endswith('.'):
893         #             m = m + "."
894         #          for Line in DNSInfo:
895         #             if isSSHFP.match(Line):
896         #                Line = "%s\t%s" % (m, Line)
897         #                F.write(Line + "\n")
898
899    # Oops, something unspeakable happened.
900    except:
901       Die(File, F, None)
902       raise
903    Done(File, F, None)
904
905 # Generate the BSMTP file
906 def GenBSMTP(accounts, File, HomePrefix):
907    F = None
908    try:
909       F = open(File + ".tmp", "w")
910      
911       # Write out the zone file entry for each user
912       for a in accounts:
913          if not 'dnsZoneEntry' in a: continue
914          if not a.is_active_user(): continue
915
916          try:
917             for z in a["dnsZoneEntry"]:
918                Split = z.lower().split()
919                if Split[1].lower() == 'in':
920                   for y in range(0, len(Split)):
921                      if Split[y] == "$":
922                         Split[y] = "\n\t"
923                   Line = " ".join(Split) + "\n"
924      
925                   Host = Split[0] + DNSZone
926                   if BSMTPCheck.match(Line) != None:
927                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
928                                   a['uid'], HomePrefix, a['uid'], Host))
929      
930          except:
931             F.write("; Errors\n")
932             pass
933   
934    # Oops, something unspeakable happened.
935    except:
936       Die(File, F, None)
937       raise
938    Done(File, F, None)
939   
940 def HostToIP(Host, mapped=True):
941
942    IPAdresses = []
943
944    if Host[1].has_key("ipHostNumber"):
945       for addr in Host[1]["ipHostNumber"]:
946          IPAdresses.append(addr)
947          if IsV6Addr.match(addr) is None and mapped == "True":
948             IPAdresses.append("::ffff:"+addr)
949
950    return IPAdresses
951
952 # Generate the ssh known hosts file
953 def GenSSHKnown(host_attrs, File, mode=None):
954    F = None
955    try:
956       OldMask = os.umask(0022)
957       F = open(File + ".tmp", "w", 0644)
958       os.umask(OldMask)
959      
960       for x in host_attrs:
961          if x[1].has_key("hostname") == 0 or \
962             x[1].has_key("sshRSAHostKey") == 0:
963             continue
964          Host = GetAttr(x, "hostname")
965          HostNames = [ Host ]
966          if Host.endswith(HostDomain):
967             HostNames.append(Host[:-(len(HostDomain) + 1)])
968      
969          # in the purpose field [[host|some other text]] (where some other text is optional)
970          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
971          # file.  But so that we don't have to add everything we link we can add an asterisk
972          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
973          # http linking it we also support [[-hostname]] entries.
974          for i in x[1].get("purpose", []):
975             m = PurposeHostField.match(i)
976             if m:
977                m = m.group(1)
978                # we ignore [[*..]] entries
979                if m.startswith('*'):
980                   continue
981                if m.startswith('-'):
982                   m = m[1:]
983                if m:
984                   HostNames.append(m)
985                   if m.endswith(HostDomain):
986                      HostNames.append(m[:-(len(HostDomain) + 1)])
987      
988          for I in x[1]["sshRSAHostKey"]:
989             if mode and mode == 'authorized_keys':
990                hosts = HostToIP(x)
991                if 'sshdistAuthKeysHost' in x[1]:
992                   hosts += x[1]['sshdistAuthKeysHost']
993                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)
994             else:
995                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
996             Line = Sanitize(Line) + "\n"
997             F.write(Line)
998    # Oops, something unspeakable happened.
999    except:
1000       Die(File, F, None)
1001       raise
1002    Done(File, F, None)
1003
1004 # Generate the debianhosts file (list of all IP addresses)
1005 def GenHosts(host_attrs, File):
1006    F = None
1007    try:
1008       OldMask = os.umask(0022)
1009       F = open(File + ".tmp", "w", 0644)
1010       os.umask(OldMask)
1011      
1012       seen = set()
1013
1014       for x in host_attrs:
1015
1016          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1017             continue
1018
1019          if not 'ipHostNumber' in x[1]:
1020             continue
1021
1022          addrs = x[1]["ipHostNumber"]
1023          for addr in addrs:
1024             if addr not in seen:
1025                seen.add(addr)
1026                addr = Sanitize(addr) + "\n"
1027                F.write(addr)
1028
1029    # Oops, something unspeakable happened.
1030    except:
1031       Die(File, F, None)
1032       raise
1033    Done(File, F, None)
1034
1035 def replaceTree(src, dst_basedir):
1036    bn = os.path.basename(src)
1037    dst = os.path.join(dst_basedir, bn)
1038    safe_rmtree(dst)
1039    shutil.copytree(src, dst)
1040
1041 def GenKeyrings(OutDir):
1042    for k in Keyrings:
1043       if os.path.isdir(k):
1044          replaceTree(k, OutDir)
1045       else:
1046          shutil.copy(k, OutDir)
1047
1048
1049 def get_accounts(ldap_conn):
1050    # Fetch all the users
1051    passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1052                    ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1053                     "gecos", "loginShell", "userPassword", "shadowLastChange",\
1054                     "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1055                     "shadowExpire", "emailForward", "latitude", "longitude",\
1056                     "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1057                     "keyFingerPrint", "privateSub", "mailDisableMessage",\
1058                     "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1059                     "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1060                     "mailContentInspectionAction", "webPassword"])
1061
1062    if passwd_attrs is None:
1063       raise UDEmptyList, "No Users"
1064    accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1065    accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1066
1067    return accounts
1068
1069 def get_hosts(ldap_conn):
1070    # Fetch all the hosts
1071    HostAttrs    = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1072                    ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1073                     "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1074
1075    if HostAttrs == None:
1076       raise UDEmptyList, "No Hosts"
1077
1078    HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1079
1080    return HostAttrs
1081
1082
1083 def make_ldap_conn():
1084    # Connect to the ldap server
1085    l = connectLDAP()
1086    # for testing purposes it's sometimes useful to pass username/password
1087    # via the environment
1088    if 'UD_CREDENTIALS' in os.environ:
1089       Pass = os.environ['UD_CREDENTIALS'].split()
1090    else:
1091       F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1092       Pass = F.readline().strip().split(" ")
1093       F.close()
1094    l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1095
1096    return l
1097
1098 def generate_all(global_dir, ldap_conn):
1099    accounts = get_accounts(ldap_conn)
1100    host_attrs = get_hosts(ldap_conn)
1101
1102    global_dir += '/'
1103    # Generate global things
1104    accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1105
1106    accounts = filter(lambda x: not IsRetired(x), accounts)
1107    #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1108
1109    CheckForward(accounts)
1110
1111    GenMailDisable(accounts, global_dir + "mail-disable")
1112    GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1113    GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1114    GenPrivate(accounts, global_dir + "debian-private")
1115    GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1116    GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1117    GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1118    GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1119    GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1120    GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1121    GenWebPassword(accounts, global_dir + "web-passwords")
1122    GenKeyrings(global_dir)
1123
1124    # Compatibility.
1125    GenForward(accounts, global_dir + "forward-alias")
1126
1127    GenAllUsers(accounts, global_dir + 'all-accounts.json')
1128    accounts = filter(lambda a: not a in accounts_disabled, accounts)
1129
1130    ssh_files = GenSSHShadow(global_dir, accounts)
1131    GenMarkers(accounts, global_dir + "markers")
1132    GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1133    GenHosts(host_attrs, global_dir + "debianhosts")
1134    GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1135
1136    GenDNS(accounts, global_dir + "dns-zone")
1137    GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1138
1139    for host in host_attrs:
1140       if not "hostname" in host[1]:
1141          continue
1142       generate_host(host, global_dir, accounts, ssh_files)
1143
1144 def generate_host(host, global_dir, accounts, ssh_files):
1145    global CurrentHost
1146
1147    CurrentHost = host[1]['hostname'][0]
1148    OutDir = global_dir + CurrentHost + '/'
1149    try:
1150       os.mkdir(OutDir)
1151    except:
1152       pass
1153
1154    # Get the group list and convert any named groups to numerics
1155    GroupList = {}
1156    for groupname in AllowedGroupsPreload.strip().split(" "):
1157       GroupList[groupname] = True
1158    if 'allowedGroups' in host[1]:
1159       for groupname in host[1]['allowedGroups']:
1160          GroupList[groupname] = True
1161    for groupname in GroupList.keys():
1162       if groupname in GroupIDMap:
1163          GroupList[str(GroupIDMap[groupname])] = True
1164
1165    ExtraList = {}
1166    if 'exportOptions' in host[1]:
1167       for extra in host[1]['exportOptions']:
1168          ExtraList[extra.upper()] = True
1169
1170    global Allowed
1171    Allowed = GroupList
1172    if Allowed == {}:
1173       Allowed = None
1174
1175    DoLink(global_dir, OutDir, "debianhosts")
1176    DoLink(global_dir, OutDir, "ssh_known_hosts")
1177    DoLink(global_dir, OutDir, "disabled-accounts")
1178
1179    sys.stdout.flush()
1180    if 'NOPASSWD' in ExtraList:
1181       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1182    else:
1183       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1184    sys.stdout.flush()
1185    grouprevmap = GenGroup(accounts, OutDir + "group")
1186    GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1187
1188    # Now we know who we're allowing on the machine, export
1189    # the relevant ssh keys
1190    GenSSHtarballs(global_dir, userlist, ssh_files, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1191
1192    if not 'NOPASSWD' in ExtraList:
1193       GenShadow(accounts, OutDir + "shadow")
1194
1195    # Link in global things
1196    if not 'NOMARKERS' in ExtraList:
1197       DoLink(global_dir, OutDir, "markers")
1198    DoLink(global_dir, OutDir, "mail-forward.cdb")
1199    DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1200    DoLink(global_dir, OutDir, "mail-disable")
1201    DoLink(global_dir, OutDir, "mail-greylist")
1202    DoLink(global_dir, OutDir, "mail-callout")
1203    DoLink(global_dir, OutDir, "mail-rbl")
1204    DoLink(global_dir, OutDir, "mail-rhsbl")
1205    DoLink(global_dir, OutDir, "mail-whitelist")
1206    DoLink(global_dir, OutDir, "all-accounts.json")
1207    GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "user-forward.cdb", 'emailForward')
1208    GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "batv-tokens.cdb", 'bATVToken')
1209    GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1210
1211    # Compatibility.
1212    DoLink(global_dir, OutDir, "forward-alias")
1213
1214    if 'DNS' in ExtraList:
1215       DoLink(global_dir, OutDir, "dns-zone")
1216       DoLink(global_dir, OutDir, "dns-sshfp")
1217
1218    if 'AUTHKEYS' in ExtraList:
1219       DoLink(global_dir, OutDir, "authorized_keys")
1220
1221    if 'BSMTP' in ExtraList:
1222       GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1223
1224    if 'PRIVATE' in ExtraList:
1225       DoLink(global_dir, OutDir, "debian-private")
1226
1227    if 'GITOLITE' in ExtraList:
1228       DoLink(global_dir, OutDir, "ssh-gitolite")
1229
1230    if 'WEB-PASSWORDS' in ExtraList:
1231       DoLink(global_dir, OutDir, "web-passwords")
1232
1233    if 'KEYRING' in ExtraList:
1234       for k in Keyrings:
1235          bn = os.path.basename(k)
1236          if os.path.isdir(k):
1237             src = os.path.join(global_dir, bn)
1238             replaceTree(src, OutDir)
1239          else:
1240             DoLink(global_dir, OutDir, bn)
1241    else:
1242       for k in Keyrings:
1243          try:
1244             bn = os.path.basename(k)
1245             target = os.path.join(OutDir, bn)
1246             if os.path.isdir(target):
1247                safe_rmtree(dst)
1248             else:
1249                posix.remove(target)
1250          except:
1251             pass
1252    DoLink(global_dir, OutDir, "last_update.trace")
1253
1254 l = make_ldap_conn()
1255
1256 mods = l.search_s('cn=log',
1257       ldap.SCOPE_ONELEVEL,
1258       '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1259       ['reqEnd'])
1260
1261 last = 0
1262
1263 # Sort the list by reqEnd
1264 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1265 # Take the last element in the array
1266 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1267
1268 # override globaldir for testing
1269 if 'UD_GENERATEDIR' in os.environ:
1270    GenerateDir = os.environ['UD_GENERATEDIR']
1271
1272 cache_last_mod = [0,0]
1273
1274 try:
1275    fd = open(os.path.join(GenerateDir, "last_update.trace"), "r")
1276    cache_last_mod=fd.read().split()
1277    fd.close()
1278 except IOError, e:
1279    if e.errno == errno.ENOENT:
1280       pass
1281    else:
1282       raise e
1283
1284 if cache_last_mod[0] >= last:
1285    fd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
1286    fd.write("%s\n%s\n" % (last, int(time.time())))
1287    fd.close()
1288    sys.exit(0)
1289
1290 # Fetch all the groups
1291 GroupIDMap = {}
1292 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1293                   ["gid", "gidNumber", "subGroup"])
1294
1295 # Generate the SubGroupMap and GroupIDMap
1296 for x in attrs:
1297    if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1298       continue
1299    if x[1].has_key("gidNumber") == 0:
1300       continue
1301    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1302    if x[1].has_key("subGroup") != 0:
1303       SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1304
1305 lock = None
1306 try:
1307    lockf = os.path.join(GenerateDir, 'ud-generate.lock')
1308    lock = get_lock( lockf )
1309    if lock is None:
1310       sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1311       sys.exit(1)
1312
1313    generate_all(GenerateDir, l)
1314
1315 finally:
1316    if lock is not None:
1317       lock.release()
1318
1319 fd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
1320 fd.write("%s\n%s\n" % (last, int(time.time())))
1321 fd.close()
1322
1323 # vim:set et:
1324 # vim:set ts=3:
1325 # vim:set shiftwidth=3: