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