UDLdap.py: more useful exception if our array assumptions are violated
[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"])
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    GenPrivate(accounts, global_dir + "debian-private")
1214    GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1215    GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1216    GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1217    GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1218    GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1219    GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1220    GenWebPassword(accounts, global_dir + "web-passwords")
1221    GenRtcPassword(accounts, global_dir + "rtc-passwords")
1222    GenTOTPSeed(accounts, global_dir + "users.oath")
1223    GenKeyrings(global_dir)
1224
1225    # Compatibility.
1226    GenForward(accounts, global_dir + "forward-alias")
1227
1228    GenAllUsers(accounts, global_dir + 'all-accounts.json')
1229    accounts = filter(lambda a: not a in accounts_disabled, accounts)
1230
1231    ssh_userkeys = GenSSHShadow(global_dir, accounts)
1232    GenMarkers(accounts, global_dir + "markers")
1233    GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1234    GenHosts(host_attrs, global_dir + "debianhosts")
1235
1236    GenDNS(accounts, global_dir + "dns-zone")
1237    GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1238
1239    setup_group_maps(ldap_conn)
1240
1241    for host in host_attrs:
1242       if not "hostname" in host[1]:
1243          continue
1244       generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1245
1246 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1247    current_host = host[1]['hostname'][0]
1248    OutDir = global_dir + current_host + '/'
1249    if not os.path.isdir(OutDir):
1250       os.mkdir(OutDir)
1251
1252    # Get the group list and convert any named groups to numerics
1253    GroupList = {}
1254    for groupname in AllowedGroupsPreload.strip().split(" "):
1255       GroupList[groupname] = True
1256    if 'allowedGroups' in host[1]:
1257       for groupname in host[1]['allowedGroups']:
1258          GroupList[groupname] = True
1259    for groupname in GroupList.keys():
1260       if groupname in GroupIDMap:
1261          GroupList[str(GroupIDMap[groupname])] = True
1262
1263    ExtraList = {}
1264    if 'exportOptions' in host[1]:
1265       for extra in host[1]['exportOptions']:
1266          ExtraList[extra.upper()] = True
1267
1268    if GroupList != {}:
1269       accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1270
1271    DoLink(global_dir, OutDir, "debianhosts")
1272    DoLink(global_dir, OutDir, "ssh_known_hosts")
1273    DoLink(global_dir, OutDir, "disabled-accounts")
1274
1275    sys.stdout.flush()
1276    if 'NOPASSWD' in ExtraList:
1277       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1278    else:
1279       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1280    sys.stdout.flush()
1281    grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1282    GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1283
1284    # Now we know who we're allowing on the machine, export
1285    # the relevant ssh keys
1286    GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1287
1288    if not 'NOPASSWD' in ExtraList:
1289       GenShadow(accounts, OutDir + "shadow")
1290
1291    # Link in global things
1292    if not 'NOMARKERS' in ExtraList:
1293       DoLink(global_dir, OutDir, "markers")
1294    DoLink(global_dir, OutDir, "mail-forward.cdb")
1295    DoLink(global_dir, OutDir, "mail-forward.db")
1296    DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1297    DoLink(global_dir, OutDir, "mail-contentinspectionaction.db")
1298    DoLink(global_dir, OutDir, "mail-disable")
1299    DoLink(global_dir, OutDir, "mail-greylist")
1300    DoLink(global_dir, OutDir, "mail-callout")
1301    DoLink(global_dir, OutDir, "mail-rbl")
1302    DoLink(global_dir, OutDir, "mail-rhsbl")
1303    DoLink(global_dir, OutDir, "mail-whitelist")
1304    DoLink(global_dir, OutDir, "all-accounts.json")
1305    GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1306    GenDBM(accounts, OutDir + "user-forward.db", 'emailForward')
1307    GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1308    GenDBM(accounts, OutDir + "batv-tokens.db", 'bATVToken')
1309    GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1310    GenDBM(accounts, OutDir + "default-mail-options.db", 'mailDefaultOptions')
1311
1312    # Compatibility.
1313    DoLink(global_dir, OutDir, "forward-alias")
1314
1315    if 'DNS' in ExtraList:
1316       DoLink(global_dir, OutDir, "dns-zone")
1317       DoLink(global_dir, OutDir, "dns-sshfp")
1318
1319    if 'AUTHKEYS' in ExtraList:
1320       DoLink(global_dir, OutDir, "authorized_keys")
1321
1322    if 'BSMTP' in ExtraList:
1323       GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1324
1325    if 'PRIVATE' in ExtraList:
1326       DoLink(global_dir, OutDir, "debian-private")
1327
1328    if 'GITOLITE' in ExtraList:
1329       GenSSHGitolite(all_accounts, all_hosts, OutDir + "ssh-gitolite", current_host=current_host)
1330    if 'exportOptions' in host[1]:
1331       for entry in host[1]['exportOptions']:
1332          v = entry.split('=',1)
1333          if v[0] != 'GITOLITE' or len(v) != 2: continue
1334          options = v[1].split(',')
1335          group = options.pop(0);
1336          gitolite_accounts = filter(lambda x: IsInGroup(x, [group], current_host), all_accounts)
1337          if not 'nohosts' in options:
1338             gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1339          else:
1340             gitolite_hosts = []
1341          command = None
1342          for opt in options:
1343             if opt.startswith('sshcmd='):
1344                command = opt.split('=',1)[1]
1345          GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(group,), sshcommand=command, current_host=current_host)
1346
1347    if 'WEB-PASSWORDS' in ExtraList:
1348       DoLink(global_dir, OutDir, "web-passwords")
1349
1350    if 'RTC-PASSWORDS' in ExtraList:
1351       DoLink(global_dir, OutDir, "rtc-passwords")
1352
1353    if 'TOTP' in ExtraList:
1354       DoLink(global_dir, OutDir, "users.oath")
1355
1356    if 'KEYRING' in ExtraList:
1357       for k in Keyrings:
1358          bn = os.path.basename(k)
1359          if os.path.isdir(k):
1360             src = os.path.join(global_dir, bn)
1361             replaceTree(src, OutDir)
1362          else:
1363             DoLink(global_dir, OutDir, bn)
1364    else:
1365       for k in Keyrings:
1366          try:
1367             bn = os.path.basename(k)
1368             target = os.path.join(OutDir, bn)
1369             if os.path.isdir(target):
1370                safe_rmtree(dst)
1371             else:
1372                posix.remove(target)
1373          except:
1374             pass
1375    DoLink(global_dir, OutDir, "last_update.trace")
1376
1377
1378 def getLastLDAPChangeTime(l):
1379    mods = l.search_s('cn=log',
1380          ldap.SCOPE_ONELEVEL,
1381          '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1382          ['reqEnd'])
1383
1384    last = 0
1385
1386    # Sort the list by reqEnd
1387    sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1388    # Take the last element in the array
1389    last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1390
1391    return last
1392
1393 def getLastKeyringChangeTime():
1394    krmod = 0
1395    for k in Keyrings:
1396       mt = os.path.getmtime(k)
1397       if mt > krmod:
1398          krmod = mt
1399
1400    return int(krmod)
1401
1402 def getLastBuildTime(gdir):
1403    cache_last_ldap_mod = 0
1404    cache_last_unix_mod = 0
1405    cache_last_run = 0
1406
1407    try:
1408       fd = open(os.path.join(gdir, "last_update.trace"), "r")
1409       cache_last_mod=fd.read().split()
1410       try:
1411          cache_last_ldap_mod = cache_last_mod[0]
1412          cache_last_unix_mod = int(cache_last_mod[1])
1413          cache_last_run = int(cache_last_mod[2])
1414       except IndexError, ValueError:
1415          pass
1416       fd.close()
1417    except IOError, e:
1418       if e.errno == errno.ENOENT:
1419          pass
1420       else:
1421          raise e
1422
1423    return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1424
1425 def mq_notify(options, message):
1426    options.section = 'dsa-udgenerate'
1427    options.config = '/etc/dsa/pubsub.conf'
1428
1429    config = Config(options)
1430    conf = {
1431       'rabbit_userid': config.username,
1432       'rabbit_password': config.password,
1433       'rabbit_virtual_host': config.vhost,
1434       'rabbit_hosts': ['pubsub02.debian.org', 'pubsub01.debian.org'],
1435       'use_ssl': False
1436    }
1437
1438    msg = {
1439       'message': message,
1440       'timestamp': int(time.time())
1441    }
1442    conn = None
1443    try:
1444       conn = Connection(conf=conf)
1445       conn.topic_send(config.topic,
1446             json.dumps(msg),
1447             exchange_name=config.exchange,
1448             timeout=5)
1449    finally:
1450       if conn:
1451          conn.close()
1452
1453 def ud_generate():
1454    parser = optparse.OptionParser()
1455    parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1456      help="Output directory.")
1457    parser.add_option("-f", "--force", dest="force", action="store_true",
1458      help="Force generation, even if no update to LDAP has happened.")
1459
1460    (options, args) = parser.parse_args()
1461    if len(args) > 0:
1462       parser.print_help()
1463       sys.exit(1)
1464
1465    if options.generatedir is not None:
1466       generate_dir = os.environ['UD_GENERATEDIR']
1467    elif 'UD_GENERATEDIR' in os.environ:
1468       generate_dir = os.environ['UD_GENERATEDIR']
1469    else:
1470       generate_dir = GenerateDir
1471
1472
1473    lockf = os.path.join(generate_dir, 'ud-generate.lock')
1474    lock = get_lock( lockf )
1475    if lock is None:
1476       sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1477       sys.exit(1)
1478
1479    l = make_ldap_conn()
1480
1481    time_started = int(time.time())
1482    ldap_last_mod = getLastLDAPChangeTime(l)
1483    unix_last_mod = getLastKeyringChangeTime()
1484    cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1485
1486    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)
1487
1488    fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1489    if need_update or options.force:
1490       msg = 'Update forced' if options.force else 'Update needed'
1491       generate_all(generate_dir, l)
1492       if use_mq:
1493          mq_notify(options, msg)
1494       last_run = int(time.time())
1495    fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1496    fd.close()
1497    sys.exit(0)
1498
1499
1500 if __name__ == "__main__":
1501    if 'UD_PROFILE' in os.environ:
1502       import cProfile
1503       import pstats
1504       cProfile.run('ud_generate()', "udg_prof")
1505       p = pstats.Stats('udg_prof')
1506       ##p.sort_stats('time').print_stats()
1507       p.sort_stats('cumulative').print_stats()
1508    else:
1509       ud_generate()
1510
1511 # vim:set et:
1512 # vim:set ts=3:
1513 # vim:set shiftwidth=3: