22eda37737d9aed20285647ef4627270db4e412b
[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    if 'sshRSAHostKey' in x[1]:
908       for I in x[1]["sshRSAHostKey"]:
909          Split = I.split()
910          if Split[0] == 'ssh-rsa':
911             Algorithm = 1
912          if Split[0] == 'ssh-dss':
913             Algorithm = 2
914          if Split[0] == 'ssh-ed25519':
915             Algorithm = 4
916          if Algorithm == None:
917             continue
918          Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
919          DNSInfo.append("%s.\t%sIN\tSSHFP\t%u 1 %s" % (hostname, TTLprefix, Algorithm, Fingerprint))
920          Fingerprint = hashlib.new('sha256', base64.decodestring(Split[1])).hexdigest()
921          DNSInfo.append("%s.\t%sIN\tSSHFP\t%u 2 %s" % (hostname, TTLprefix, Algorithm, Fingerprint))
922
923    if 'architecture' in x[1]:
924       Arch = GetAttr(x, "architecture")
925       Mach = ""
926       if x[1].has_key("machine"):
927          Mach = " " + GetAttr(x, "machine")
928       DNSInfo.append("%s.\t%sIN\tHINFO\t\"%s%s\" \"%s\"" % (hostname, TTLprefix, Arch, Mach, "Debian"))
929
930    if x[1].has_key("mXRecord"):
931       for I in x[1]["mXRecord"]:
932          if I in MX_remap:
933             for e in MX_remap[I]:
934                DNSInfo.append("%s.\t%sIN\tMX\t%s" % (hostname, TTLprefix, e))
935          else:
936             DNSInfo.append("%s.\t%sIN\tMX\t%s" % (hostname, TTLprefix, I))
937
938    return DNSInfo
939
940 # Generate the DNS records
941 def GenZoneRecords(host_attrs, File):
942    F = None
943    try:
944       F = open(File + ".tmp", "w")
945
946       # Fetch all the hosts
947       for x in host_attrs:
948          if x[1].has_key("hostname") == 0:
949             continue
950
951          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
952             continue
953
954          for Line in ExtractDNSInfo(x):
955             F.write(Line + "\n")
956
957         # this would write sshfp lines for services on machines
958         # but we can't yet, since some are cnames and we'll make
959         # an invalid zonefile
960         #
961         # for i in x[1].get("purpose", []):
962         #    m = PurposeHostField.match(i)
963         #    if m:
964         #       m = m.group(1)
965         #       # we ignore [[*..]] entries
966         #       if m.startswith('*'):
967         #          continue
968         #       if m.startswith('-'):
969         #          m = m[1:]
970         #       if m:
971         #          if not m.endswith(HostDomain):
972         #             continue
973         #          if not m.endswith('.'):
974         #             m = m + "."
975         #          for Line in DNSInfo:
976         #             if isSSHFP.match(Line):
977         #                Line = "%s\t%s" % (m, Line)
978         #                F.write(Line + "\n")
979
980    # Oops, something unspeakable happened.
981    except:
982       Die(File, F, None)
983       raise
984    Done(File, F, None)
985
986 # Generate the BSMTP file
987 def GenBSMTP(accounts, File, HomePrefix):
988    F = None
989    try:
990       F = open(File + ".tmp", "w")
991      
992       # Write out the zone file entry for each user
993       for a in accounts:
994          if not 'dnsZoneEntry' in a: continue
995          if not a.is_active_user(): continue
996
997          try:
998             for z in a["dnsZoneEntry"]:
999                Split = z.lower().split()
1000                if Split[1].lower() == 'in':
1001                   for y in range(0, len(Split)):
1002                      if Split[y] == "$":
1003                         Split[y] = "\n\t"
1004                   Line = " ".join(Split) + "\n"
1005      
1006                   Host = Split[0] + DNSZone
1007                   if BSMTPCheck.match(Line) != None:
1008                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
1009                                   a['uid'], HomePrefix, a['uid'], Host))
1010      
1011          except:
1012             F.write("; Errors\n")
1013             pass
1014   
1015    # Oops, something unspeakable happened.
1016    except:
1017       Die(File, F, None)
1018       raise
1019    Done(File, F, None)
1020   
1021 def HostToIP(Host, mapped=True):
1022
1023    IPAdresses = []
1024
1025    if Host[1].has_key("ipHostNumber"):
1026       for addr in Host[1]["ipHostNumber"]:
1027          IPAdresses.append(addr)
1028          if not is_ipv6_addr(addr) and mapped == "True":
1029             IPAdresses.append("::ffff:"+addr)
1030
1031    return IPAdresses
1032
1033 # Generate the ssh known hosts file
1034 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
1035    F = None
1036    try:
1037       OldMask = os.umask(0022)
1038       F = open(File + ".tmp", "w", 0644)
1039       os.umask(OldMask)
1040      
1041       for x in host_attrs:
1042          if x[1].has_key("hostname") == 0 or \
1043             x[1].has_key("sshRSAHostKey") == 0:
1044             continue
1045          Host = GetAttr(x, "hostname")
1046          HostNames = [ Host ]
1047          if Host.endswith(HostDomain):
1048             HostNames.append(Host[:-(len(HostDomain) + 1)])
1049      
1050          # in the purpose field [[host|some other text]] (where some other text is optional)
1051          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1052          # file.  But so that we don't have to add everything we link we can add an asterisk
1053          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
1054          # http linking it we also support [[-hostname]] entries.
1055          for i in x[1].get("purpose", []):
1056             m = PurposeHostField.match(i)
1057             if m:
1058                m = m.group(1)
1059                # we ignore [[*..]] entries
1060                if m.startswith('*'):
1061                   continue
1062                if m.startswith('-'):
1063                   m = m[1:]
1064                if m:
1065                   HostNames.append(m)
1066                   if m.endswith(HostDomain):
1067                      HostNames.append(m[:-(len(HostDomain) + 1)])
1068      
1069          for I in x[1]["sshRSAHostKey"]:
1070             if mode and mode == 'authorized_keys':
1071                hosts = HostToIP(x)
1072                if 'sshdistAuthKeysHost' in x[1]:
1073                   hosts += x[1]['sshdistAuthKeysHost']
1074                clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1075                clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1076                Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1077             else:
1078                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1079             Line = Sanitize(Line) + "\n"
1080             F.write(Line)
1081    # Oops, something unspeakable happened.
1082    except:
1083       Die(File, F, None)
1084       raise
1085    Done(File, F, None)
1086
1087 # Generate the debianhosts file (list of all IP addresses)
1088 def GenHosts(host_attrs, File):
1089    F = None
1090    try:
1091       OldMask = os.umask(0022)
1092       F = open(File + ".tmp", "w", 0644)
1093       os.umask(OldMask)
1094      
1095       seen = set()
1096
1097       for x in host_attrs:
1098
1099          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1100             continue
1101
1102          if not 'ipHostNumber' in x[1]:
1103             continue
1104
1105          addrs = x[1]["ipHostNumber"]
1106          for addr in addrs:
1107             if addr not in seen:
1108                seen.add(addr)
1109                addr = Sanitize(addr) + "\n"
1110                F.write(addr)
1111
1112    # Oops, something unspeakable happened.
1113    except:
1114       Die(File, F, None)
1115       raise
1116    Done(File, F, None)
1117
1118 def replaceTree(src, dst_basedir):
1119    bn = os.path.basename(src)
1120    dst = os.path.join(dst_basedir, bn)
1121    safe_rmtree(dst)
1122    shutil.copytree(src, dst)
1123
1124 def GenKeyrings(OutDir):
1125    for k in Keyrings:
1126       if os.path.isdir(k):
1127          replaceTree(k, OutDir)
1128       else:
1129          shutil.copy(k, OutDir)
1130
1131
1132 def get_accounts(ldap_conn):
1133    # Fetch all the users
1134    passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1135                    ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1136                     "gecos", "loginShell", "userPassword", "shadowLastChange",\
1137                     "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1138                     "shadowExpire", "emailForward", "latitude", "longitude",\
1139                     "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1140                     "keyFingerPrint", "privateSub", "mailDisableMessage",\
1141                     "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1142                     "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1143                     "mailContentInspectionAction", "webPassword", "rtcPassword",\
1144                     "bATVToken", "totpSeed"])
1145
1146    if passwd_attrs is None:
1147       raise UDEmptyList, "No Users"
1148    accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1149    accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1150
1151    return accounts
1152
1153 def get_hosts(ldap_conn):
1154    # Fetch all the hosts
1155    HostAttrs    = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1156                    ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1157                     "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1158
1159    if HostAttrs == None:
1160       raise UDEmptyList, "No Hosts"
1161
1162    HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1163
1164    return HostAttrs
1165
1166
1167 def make_ldap_conn():
1168    # Connect to the ldap server
1169    l = connectLDAP()
1170    # for testing purposes it's sometimes useful to pass username/password
1171    # via the environment
1172    if 'UD_CREDENTIALS' in os.environ:
1173       Pass = os.environ['UD_CREDENTIALS'].split()
1174    else:
1175       F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1176       Pass = F.readline().strip().split(" ")
1177       F.close()
1178    l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1179
1180    return l
1181
1182
1183
1184 def setup_group_maps(l):
1185    # Fetch all the groups
1186    group_id_map = {}
1187    subgroup_map = {}
1188    attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1189                      ["gid", "gidNumber", "subGroup"])
1190
1191    # Generate the subgroup_map and group_id_map
1192    for x in attrs:
1193       if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1194          continue
1195       if x[1].has_key("gidNumber") == 0:
1196          continue
1197       group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1198       if x[1].has_key("subGroup") != 0:
1199          subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1200
1201    global SubGroupMap
1202    global GroupIDMap
1203    SubGroupMap = subgroup_map
1204    GroupIDMap = group_id_map
1205
1206 def generate_all(global_dir, ldap_conn):
1207    accounts = get_accounts(ldap_conn)
1208    host_attrs = get_hosts(ldap_conn)
1209
1210    global_dir += '/'
1211    # Generate global things
1212    accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1213
1214    accounts = filter(lambda x: not IsRetired(x), accounts)
1215
1216    CheckForward(accounts)
1217
1218    GenMailDisable(accounts, global_dir + "mail-disable")
1219    GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1220    GenDBM(accounts, global_dir + "mail-forward.db", 'emailForward')
1221    GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1222    GenDBM(accounts, global_dir + "mail-contentinspectionaction.db", 'mailContentInspectionAction')
1223    GenPrivate(accounts, global_dir + "debian-private")
1224    GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1225    GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1226    GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1227    GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1228    GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1229    GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1230    GenWebPassword(accounts, global_dir + "web-passwords")
1231    GenRtcPassword(accounts, global_dir + "rtc-passwords")
1232    GenTOTPSeed(accounts, global_dir + "users.oath")
1233    GenKeyrings(global_dir)
1234
1235    # Compatibility.
1236    GenForward(accounts, global_dir + "forward-alias")
1237
1238    GenAllUsers(accounts, global_dir + 'all-accounts.json')
1239    accounts = filter(lambda a: not a in accounts_disabled, accounts)
1240
1241    ssh_userkeys = GenSSHShadow(global_dir, accounts)
1242    GenMarkers(accounts, global_dir + "markers")
1243    GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1244    GenHosts(host_attrs, global_dir + "debianhosts")
1245
1246    GenDNS(accounts, global_dir + "dns-zone")
1247    GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1248
1249    setup_group_maps(ldap_conn)
1250
1251    for host in host_attrs:
1252       if not "hostname" in host[1]:
1253          continue
1254       generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1255
1256 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1257    current_host = host[1]['hostname'][0]
1258    OutDir = global_dir + current_host + '/'
1259    if not os.path.isdir(OutDir):
1260       os.mkdir(OutDir)
1261
1262    # Get the group list and convert any named groups to numerics
1263    GroupList = {}
1264    for groupname in AllowedGroupsPreload.strip().split(" "):
1265       GroupList[groupname] = True
1266    if 'allowedGroups' in host[1]:
1267       for groupname in host[1]['allowedGroups']:
1268          GroupList[groupname] = True
1269    for groupname in GroupList.keys():
1270       if groupname in GroupIDMap:
1271          GroupList[str(GroupIDMap[groupname])] = True
1272
1273    ExtraList = {}
1274    if 'exportOptions' in host[1]:
1275       for extra in host[1]['exportOptions']:
1276          ExtraList[extra.upper()] = True
1277
1278    if GroupList != {}:
1279       accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1280
1281    DoLink(global_dir, OutDir, "debianhosts")
1282    DoLink(global_dir, OutDir, "ssh_known_hosts")
1283    DoLink(global_dir, OutDir, "disabled-accounts")
1284
1285    sys.stdout.flush()
1286    if 'NOPASSWD' in ExtraList:
1287       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1288    else:
1289       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1290    sys.stdout.flush()
1291    grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1292    GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1293
1294    # Now we know who we're allowing on the machine, export
1295    # the relevant ssh keys
1296    GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1297
1298    if not 'NOPASSWD' in ExtraList:
1299       GenShadow(accounts, OutDir + "shadow")
1300
1301    # Link in global things
1302    if not 'NOMARKERS' in ExtraList:
1303       DoLink(global_dir, OutDir, "markers")
1304    DoLink(global_dir, OutDir, "mail-forward.cdb")
1305    DoLink(global_dir, OutDir, "mail-forward.db")
1306    DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1307    DoLink(global_dir, OutDir, "mail-contentinspectionaction.db")
1308    DoLink(global_dir, OutDir, "mail-disable")
1309    DoLink(global_dir, OutDir, "mail-greylist")
1310    DoLink(global_dir, OutDir, "mail-callout")
1311    DoLink(global_dir, OutDir, "mail-rbl")
1312    DoLink(global_dir, OutDir, "mail-rhsbl")
1313    DoLink(global_dir, OutDir, "mail-whitelist")
1314    DoLink(global_dir, OutDir, "all-accounts.json")
1315    GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1316    GenDBM(accounts, OutDir + "user-forward.db", 'emailForward')
1317    GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1318    GenDBM(accounts, OutDir + "batv-tokens.db", 'bATVToken')
1319    GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1320    GenDBM(accounts, OutDir + "default-mail-options.db", 'mailDefaultOptions')
1321
1322    # Compatibility.
1323    DoLink(global_dir, OutDir, "forward-alias")
1324
1325    if 'DNS' in ExtraList:
1326       DoLink(global_dir, OutDir, "dns-zone")
1327       DoLink(global_dir, OutDir, "dns-sshfp")
1328
1329    if 'AUTHKEYS' in ExtraList:
1330       DoLink(global_dir, OutDir, "authorized_keys")
1331
1332    if 'BSMTP' in ExtraList:
1333       GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1334
1335    if 'PRIVATE' in ExtraList:
1336       DoLink(global_dir, OutDir, "debian-private")
1337
1338    if 'GITOLITE' in ExtraList:
1339       GenSSHGitolite(all_accounts, all_hosts, OutDir + "ssh-gitolite", current_host=current_host)
1340    if 'exportOptions' in host[1]:
1341       for entry in host[1]['exportOptions']:
1342          v = entry.split('=',1)
1343          if v[0] != 'GITOLITE' or len(v) != 2: continue
1344          options = v[1].split(',')
1345          group = options.pop(0);
1346          gitolite_accounts = filter(lambda x: IsInGroup(x, [group], current_host), all_accounts)
1347          if not 'nohosts' in options:
1348             gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1349          else:
1350             gitolite_hosts = []
1351          command = None
1352          for opt in options:
1353             if opt.startswith('sshcmd='):
1354                command = opt.split('=',1)[1]
1355          GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(group,), sshcommand=command, current_host=current_host)
1356
1357    if 'WEB-PASSWORDS' in ExtraList:
1358       DoLink(global_dir, OutDir, "web-passwords")
1359
1360    if 'RTC-PASSWORDS' in ExtraList:
1361       DoLink(global_dir, OutDir, "rtc-passwords")
1362
1363    if 'TOTP' in ExtraList:
1364       DoLink(global_dir, OutDir, "users.oath")
1365
1366    if 'KEYRING' in ExtraList:
1367       for k in Keyrings:
1368          bn = os.path.basename(k)
1369          if os.path.isdir(k):
1370             src = os.path.join(global_dir, bn)
1371             replaceTree(src, OutDir)
1372          else:
1373             DoLink(global_dir, OutDir, bn)
1374    else:
1375       for k in Keyrings:
1376          try:
1377             bn = os.path.basename(k)
1378             target = os.path.join(OutDir, bn)
1379             if os.path.isdir(target):
1380                safe_rmtree(dst)
1381             else:
1382                posix.remove(target)
1383          except:
1384             pass
1385    DoLink(global_dir, OutDir, "last_update.trace")
1386
1387
1388 def getLastLDAPChangeTime(l):
1389    mods = l.search_s('cn=log',
1390          ldap.SCOPE_ONELEVEL,
1391          '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1392          ['reqEnd'])
1393
1394    last = 0
1395
1396    # Sort the list by reqEnd
1397    sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1398    # Take the last element in the array
1399    last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1400
1401    return last
1402
1403 def getLastKeyringChangeTime():
1404    krmod = 0
1405    for k in Keyrings:
1406       mt = os.path.getmtime(k)
1407       if mt > krmod:
1408          krmod = mt
1409
1410    return int(krmod)
1411
1412 def getLastBuildTime(gdir):
1413    cache_last_ldap_mod = 0
1414    cache_last_unix_mod = 0
1415    cache_last_run = 0
1416
1417    try:
1418       fd = open(os.path.join(gdir, "last_update.trace"), "r")
1419       cache_last_mod=fd.read().split()
1420       try:
1421          cache_last_ldap_mod = cache_last_mod[0]
1422          cache_last_unix_mod = int(cache_last_mod[1])
1423          cache_last_run = int(cache_last_mod[2])
1424       except IndexError, ValueError:
1425          pass
1426       fd.close()
1427    except IOError, e:
1428       if e.errno == errno.ENOENT:
1429          pass
1430       else:
1431          raise e
1432
1433    return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1434
1435 def mq_notify(options, message):
1436    options.section = 'dsa-udgenerate'
1437    options.config = '/etc/dsa/pubsub.conf'
1438
1439    config = Config(options)
1440    conf = {
1441       'rabbit_userid': config.username,
1442       'rabbit_password': config.password,
1443       'rabbit_virtual_host': config.vhost,
1444       'rabbit_hosts': ['pubsub02.debian.org', 'pubsub01.debian.org'],
1445       'use_ssl': False
1446    }
1447
1448    msg = {
1449       'message': message,
1450       'timestamp': int(time.time())
1451    }
1452    conn = None
1453    try:
1454       conn = Connection(conf=conf)
1455       conn.topic_send(config.topic,
1456             json.dumps(msg),
1457             exchange_name=config.exchange,
1458             timeout=5)
1459    finally:
1460       if conn:
1461          conn.close()
1462
1463 def ud_generate():
1464    parser = optparse.OptionParser()
1465    parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1466      help="Output directory.")
1467    parser.add_option("-f", "--force", dest="force", action="store_true",
1468      help="Force generation, even if no update to LDAP has happened.")
1469
1470    (options, args) = parser.parse_args()
1471    if len(args) > 0:
1472       parser.print_help()
1473       sys.exit(1)
1474
1475    if options.generatedir is not None:
1476       generate_dir = os.environ['UD_GENERATEDIR']
1477    elif 'UD_GENERATEDIR' in os.environ:
1478       generate_dir = os.environ['UD_GENERATEDIR']
1479    else:
1480       generate_dir = GenerateDir
1481
1482
1483    lockf = os.path.join(generate_dir, 'ud-generate.lock')
1484    lock = get_lock( lockf )
1485    if lock is None:
1486       sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1487       sys.exit(1)
1488
1489    l = make_ldap_conn()
1490
1491    time_started = int(time.time())
1492    ldap_last_mod = getLastLDAPChangeTime(l)
1493    unix_last_mod = getLastKeyringChangeTime()
1494    cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1495
1496    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)
1497
1498    fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1499    if need_update or options.force:
1500       msg = 'Update forced' if options.force else 'Update needed'
1501       generate_all(generate_dir, l)
1502       if use_mq:
1503          mq_notify(options, msg)
1504       last_run = int(time.time())
1505    fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1506    fd.close()
1507    sys.exit(0)
1508
1509
1510 if __name__ == "__main__":
1511    if 'UD_PROFILE' in os.environ:
1512       import cProfile
1513       import pstats
1514       cProfile.run('ud_generate()', "udg_prof")
1515       p = pstats.Stats('udg_prof')
1516       ##p.sort_stats('time').print_stats()
1517       p.sort_stats('cumulative').print_stats()
1518    else:
1519       ud_generate()
1520
1521 # vim:set et:
1522 # vim:set ts=3:
1523 # vim:set shiftwidth=3: