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