Merge branch 'fordsa' of https://git.adam-barratt.org.uk/git/mirror/userdir-ldap
[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 '__author__' not 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 'supplementaryGid' not 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 is not None:
188       F.close()
189    if Fdb is not 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 is not None:
202       F.close()
203       os.rename(File + ".tmp", File)
204    if Fdb is not 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 is 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 'sshRSAAuthKey' not 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 'sshRSAHostKey' not 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 'sshRSAAuthKey' not 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 'webPassword' not 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 'rtcPassword' not 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 'totpSeed' not 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 'supplementaryGid' not 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 x not 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 'emailForward' not 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 'emailForward' not 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    prefix = ["/usr/bin/eatmydata"] if os.path.exists('/usr/bin/eatmydata') else []
649    # nothing else does the fsync stuff, so why do it here?
650    Fdb = subprocess.Popen(prefix + ["cdbmake", File, "%s.tmp" % File],
651                           preexec_fn=lambda: os.umask(0022),
652                           stdin=subprocess.PIPE)
653    try:
654       # Write out the email address for each user
655       for a in accounts:
656          if key not in a: continue
657          value = a[key]
658          user = a['uid']
659          Fdb.stdin.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
660
661       Fdb.stdin.write("\n")
662    finally:
663       Fdb.stdin.close()
664       if Fdb.wait() != 0:
665          raise Exception("cdbmake gave an error")
666
667 def GenDBM(accounts, File, key):
668    Fdb = None
669    OldMask = os.umask(0022)
670    fn = os.path.join(File).encode('ascii', 'ignore')
671    try:
672       posix.remove(fn)
673    except:
674       pass
675
676    try:
677       Fdb = dbm.open(fn, "c")
678       os.umask(OldMask)
679
680       # Write out the email address for each user
681       for a in accounts:
682          if key not in a: continue
683          value = a[key]
684          user = a['uid']
685          Fdb[user] = value
686
687       Fdb.close()
688    except:
689       # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
690       os.remove(File + ".db")
691       raise
692    # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
693    os.rename (File + ".db", File)
694
695 # Generate the anon XEarth marker file
696 def GenMarkers(accounts, File):
697    F = None
698    try:
699       F = open(File + ".tmp", "w")
700
701       # Write out the position for each user
702       for a in accounts:
703          if not ('latitude' in a and 'longitude' in a): continue
704          try:
705             Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
706             Line = Sanitize(Line) + "\n"
707             F.write(Line)
708          except:
709             pass
710   
711    # Oops, something unspeakable happened.
712    except:
713       Die(File, F, None)
714       raise
715    Done(File, F, None)
716
717 # Generate the debian-private subscription list
718 def GenPrivate(accounts, File):
719    F = None
720    try:
721       F = open(File + ".tmp", "w")
722
723       # Write out the position for each user
724       for a in accounts:
725          if not a.is_active_user(): continue
726          if a.is_guest_account(): continue
727          if 'privateSub' not in a: continue
728          try:
729             Line = "%s"%(a['privateSub'])
730             Line = Sanitize(Line) + "\n"
731             F.write(Line)
732          except:
733             pass
734   
735    # Oops, something unspeakable happened.
736    except:
737       Die(File, F, None)
738       raise
739    Done(File, F, None)
740
741 # Generate a list of locked accounts
742 def GenDisabledAccounts(accounts, File):
743    F = None
744    try:
745       F = open(File + ".tmp", "w")
746       disabled_accounts = []
747
748       # Fetch all the users
749       for a in accounts:
750          if a.pw_active(): continue
751          Line = "%s:%s" % (a['uid'], "Account is locked")
752          disabled_accounts.append(a)
753          F.write(Sanitize(Line) + "\n")
754
755    # Oops, something unspeakable happened.
756    except:
757       Die(File, F, None)
758       raise
759    Done(File, F, None)
760    return disabled_accounts
761
762 # Generate the list of local addresses that refuse all mail
763 def GenMailDisable(accounts, File):
764    F = None
765    try:
766       F = open(File + ".tmp", "w")
767
768       for a in accounts:
769          if 'mailDisableMessage' not in a: continue
770          Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
771          Line = Sanitize(Line) + "\n"
772          F.write(Line)
773
774    # Oops, something unspeakable happened.
775    except:
776       Die(File, F, None)
777       raise
778    Done(File, F, None)
779
780 # Generate a list of uids that should have boolean affects applied
781 def GenMailBool(accounts, File, key):
782    F = None
783    try:
784       F = open(File + ".tmp", "w")
785
786       for a in accounts:
787          if key not in a: continue
788          if not a[key] == 'TRUE': continue
789          Line = "%s"%(a['uid'])
790          Line = Sanitize(Line) + "\n"
791          F.write(Line)
792
793    # Oops, something unspeakable happened.
794    except:
795       Die(File, F, None)
796       raise
797    Done(File, F, None)
798
799 # Generate a list of hosts for RBL or whitelist purposes.
800 def GenMailList(accounts, File, key):
801    F = None
802    try:
803       F = open(File + ".tmp", "w")
804
805       if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
806       else:                      validregex = re.compile('^[-\w.]+$')
807
808       for a in accounts:
809          if key not in a: continue
810
811          filtered = filter(lambda z: validregex.match(z), a[key])
812          if len(filtered) == 0: continue
813          if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
814          line = a['uid'] + ': ' + ' : '.join(filtered)
815          line = Sanitize(line) + "\n"
816          F.write(line)
817
818    # Oops, something unspeakable happened.
819    except:
820       Die(File, F, None)
821       raise
822    Done(File, F, None)
823
824 def isRoleAccount(account):
825    return 'debianRoleAccount' in account['objectClass']
826
827 # Generate the DNS Zone file
828 def GenDNS(accounts, File):
829    F = None
830    try:
831       F = open(File + ".tmp", "w")
832
833       # Fetch all the users
834       RRs = {}
835
836       # Write out the zone file entry for each user
837       for a in accounts:
838          if 'dnsZoneEntry' not in a: continue
839          if not a.is_active_user() and not isRoleAccount(a): continue
840          if a.is_guest_account(): continue
841
842          try:
843             F.write("; %s\n"%(a.email_address()))
844             for z in a["dnsZoneEntry"]:
845                Split = z.lower().split()
846                if Split[1].lower() == 'in':
847                   Line = " ".join(Split) + "\n"
848                   F.write(Line)
849
850                   Host = Split[0] + DNSZone
851                   if BSMTPCheck.match(Line) is not None:
852                      F.write("; Has BSMTP\n")
853
854                   # Write some identification information
855                   if not RRs.has_key(Host):
856                      if Split[2].lower() in ["a", "aaaa"]:
857                         Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
858                         for y in a["keyFingerPrint"]:
859                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
860                            F.write(Line)
861                         RRs[Host] = 1
862                else:
863                   Line = "; Err %s"%(str(Split))
864                   F.write(Line)
865
866             F.write("\n")
867          except Exception, e:
868             F.write("; Errors:\n")
869             for line in str(e).split("\n"):
870                F.write("; %s\n"%(line))
871             pass
872   
873    # Oops, something unspeakable happened.
874    except:
875       Die(File, F, None)
876       raise
877    Done(File, F, None)
878
879 def is_ipv6_addr(i):
880    try:
881       socket.inet_pton(socket.AF_INET6, i)
882    except socket.error:
883       return False
884    return True
885
886 def ExtractDNSInfo(x):
887    hostname = GetAttr(x, "hostname")
888
889    TTLprefix="\t"
890    if 'dnsTTL' in x[1]:
891       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
892
893    DNSInfo = []
894    if x[1].has_key("ipHostNumber"):
895       for I in x[1]["ipHostNumber"]:
896          if is_ipv6_addr(I):
897             DNSInfo.append("%s.\t%sIN\tAAAA\t%s" % (hostname, TTLprefix, I))
898          else:
899             DNSInfo.append("%s.\t%sIN\tA\t%s" % (hostname, TTLprefix, I))
900
901    Algorithm = None
902
903    ssh_hostnames = [ hostname ]
904    if x[1].has_key("sshfpHostname"):
905       ssh_hostnames += [ h for h in x[1]["sshfpHostname"] ]
906
907    if 'sshRSAHostKey' in x[1]:
908       for I in x[1]["sshRSAHostKey"]:
909          Split = I.split()
910          key_prefix = Split[0]
911          key = base64.decodestring(Split[1])
912
913          # RFC4255
914          # https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml
915          if key_prefix == 'ssh-rsa':
916             Algorithm = 1
917          if key_prefix == 'ssh-dss':
918             Algorithm = 2
919          if key_prefix == 'ssh-ed25519':
920             Algorithm = 4
921          if Algorithm is None:
922             continue
923          # and more from the registry
924          sshfp_digest_codepoints = [ (1, 'sha1'), (2, 'sha256') ]
925
926          fingerprints = [ ( digest_codepoint, hashlib.new(algorithm, key).hexdigest() ) for digest_codepoint, algorithm in sshfp_digest_codepoints ]
927          for h in ssh_hostnames:
928             for digest_codepoint, fingerprint in fingerprints:
929                DNSInfo.append("%s.\t%sIN\tSSHFP\t%u %d %s" % (h, TTLprefix, Algorithm, digest_codepoint, fingerprint))
930
931    if 'architecture' in x[1]:
932       Arch = GetAttr(x, "architecture")
933       Mach = ""
934       if x[1].has_key("machine"):
935          Mach = " " + GetAttr(x, "machine")
936       DNSInfo.append("%s.\t%sIN\tHINFO\t\"%s%s\" \"%s\"" % (hostname, TTLprefix, Arch, Mach, "Debian"))
937
938    if x[1].has_key("mXRecord"):
939       for I in x[1]["mXRecord"]:
940          if I in MX_remap:
941             for e in MX_remap[I]:
942                DNSInfo.append("%s.\t%sIN\tMX\t%s" % (hostname, TTLprefix, e))
943          else:
944             DNSInfo.append("%s.\t%sIN\tMX\t%s" % (hostname, TTLprefix, I))
945
946    return DNSInfo
947
948 # Generate the DNS records
949 def GenZoneRecords(host_attrs, File):
950    F = None
951    try:
952       F = open(File + ".tmp", "w")
953
954       # Fetch all the hosts
955       for x in host_attrs:
956          if x[1].has_key("hostname") == 0:
957             continue
958
959          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
960             continue
961
962          for Line in ExtractDNSInfo(x):
963             F.write(Line + "\n")
964
965    # Oops, something unspeakable happened.
966    except:
967       Die(File, F, None)
968       raise
969    Done(File, F, None)
970
971 # Generate the BSMTP file
972 def GenBSMTP(accounts, File, HomePrefix):
973    F = None
974    try:
975       F = open(File + ".tmp", "w")
976      
977       # Write out the zone file entry for each user
978       for a in accounts:
979          if 'dnsZoneEntry' not in a: continue
980          if not a.is_active_user(): continue
981
982          try:
983             for z in a["dnsZoneEntry"]:
984                Split = z.lower().split()
985                if Split[1].lower() == 'in':
986                   for y in range(0, len(Split)):
987                      if Split[y] == "$":
988                         Split[y] = "\n\t"
989                   Line = " ".join(Split) + "\n"
990      
991                   Host = Split[0] + DNSZone
992                   if BSMTPCheck.match(Line) is not None:
993                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
994                                   a['uid'], HomePrefix, a['uid'], Host))
995      
996          except:
997             F.write("; Errors\n")
998             pass
999   
1000    # Oops, something unspeakable happened.
1001    except:
1002       Die(File, F, None)
1003       raise
1004    Done(File, F, None)
1005   
1006 def HostToIP(Host, mapped=True):
1007
1008    IPAdresses = []
1009
1010    if Host[1].has_key("ipHostNumber"):
1011       for addr in Host[1]["ipHostNumber"]:
1012          IPAdresses.append(addr)
1013          if not is_ipv6_addr(addr) and mapped == "True":
1014             IPAdresses.append("::ffff:"+addr)
1015
1016    return IPAdresses
1017
1018 # Generate the ssh known hosts file
1019 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
1020    F = None
1021    try:
1022       OldMask = os.umask(0022)
1023       F = open(File + ".tmp", "w", 0644)
1024       os.umask(OldMask)
1025      
1026       for x in host_attrs:
1027          if x[1].has_key("hostname") == 0 or \
1028             x[1].has_key("sshRSAHostKey") == 0:
1029             continue
1030          Host = GetAttr(x, "hostname")
1031          HostNames = [ Host ]
1032          if Host.endswith(HostDomain):
1033             HostNames.append(Host[:-(len(HostDomain) + 1)])
1034      
1035          # in the purpose field [[host|some other text]] (where some other text is optional)
1036          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1037          # file.  But so that we don't have to add everything we link we can add an asterisk
1038          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
1039          # http linking it we also support [[-hostname]] entries.
1040          for i in x[1].get("purpose", []):
1041             m = PurposeHostField.match(i)
1042             if m:
1043                m = m.group(1)
1044                # we ignore [[*..]] entries
1045                if m.startswith('*'):
1046                   continue
1047                if m.startswith('-'):
1048                   m = m[1:]
1049                if m:
1050                   HostNames.append(m)
1051                   if m.endswith(HostDomain):
1052                      HostNames.append(m[:-(len(HostDomain) + 1)])
1053      
1054          for I in x[1]["sshRSAHostKey"]:
1055             if mode and mode == 'authorized_keys':
1056                hosts = HostToIP(x)
1057                if 'sshdistAuthKeysHost' in x[1]:
1058                   hosts += x[1]['sshdistAuthKeysHost']
1059                clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1060                clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1061                Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1062             else:
1063                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1064             Line = Sanitize(Line) + "\n"
1065             F.write(Line)
1066    # Oops, something unspeakable happened.
1067    except:
1068       Die(File, F, None)
1069       raise
1070    Done(File, F, None)
1071
1072 # Generate the debianhosts file (list of all IP addresses)
1073 def GenHosts(host_attrs, File):
1074    F = None
1075    try:
1076       OldMask = os.umask(0022)
1077       F = open(File + ".tmp", "w", 0644)
1078       os.umask(OldMask)
1079      
1080       seen = set()
1081
1082       for x in host_attrs:
1083
1084          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1085             continue
1086
1087          if 'ipHostNumber' not in x[1]:
1088             continue
1089
1090          addrs = x[1]["ipHostNumber"]
1091          for addr in addrs:
1092             if addr not in seen:
1093                seen.add(addr)
1094                addr = Sanitize(addr) + "\n"
1095                F.write(addr)
1096
1097    # Oops, something unspeakable happened.
1098    except:
1099       Die(File, F, None)
1100       raise
1101    Done(File, F, None)
1102
1103 def replaceTree(src, dst_basedir):
1104    bn = os.path.basename(src)
1105    dst = os.path.join(dst_basedir, bn)
1106    safe_rmtree(dst)
1107    shutil.copytree(src, dst)
1108
1109 def GenKeyrings(OutDir):
1110    for k in Keyrings:
1111       if os.path.isdir(k):
1112          replaceTree(k, OutDir)
1113       else:
1114          shutil.copy(k, OutDir)
1115
1116
1117 def get_accounts(ldap_conn):
1118    # Fetch all the users
1119    passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1120                    ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1121                     "gecos", "loginShell", "userPassword", "shadowLastChange",\
1122                     "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1123                     "shadowExpire", "emailForward", "latitude", "longitude",\
1124                     "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1125                     "keyFingerPrint", "privateSub", "mailDisableMessage",\
1126                     "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1127                     "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1128                     "mailContentInspectionAction", "webPassword", "rtcPassword",\
1129                     "bATVToken", "totpSeed", "mailDefaultOptions"])
1130
1131    if passwd_attrs is None:
1132       raise UDEmptyList, "No Users"
1133    accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1134    accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1135
1136    return accounts
1137
1138 def get_hosts(ldap_conn):
1139    # Fetch all the hosts
1140    HostAttrs    = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1141                    ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1142                     "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture",
1143                     "sshfpHostname"])
1144
1145    if HostAttrs is None:
1146       raise UDEmptyList, "No Hosts"
1147
1148    HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1149
1150    return HostAttrs
1151
1152
1153 def make_ldap_conn():
1154    # Connect to the ldap server
1155    l = connectLDAP()
1156    # for testing purposes it's sometimes useful to pass username/password
1157    # via the environment
1158    if 'UD_CREDENTIALS' in os.environ:
1159       Pass = os.environ['UD_CREDENTIALS'].split()
1160    else:
1161       F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1162       Pass = F.readline().strip().split(" ")
1163       F.close()
1164    l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1165
1166    return l
1167
1168
1169
1170 def setup_group_maps(l):
1171    # Fetch all the groups
1172    group_id_map = {}
1173    subgroup_map = {}
1174    attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1175                      ["gid", "gidNumber", "subGroup"])
1176
1177    # Generate the subgroup_map and group_id_map
1178    for x in attrs:
1179       if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1180          continue
1181       if x[1].has_key("gidNumber") == 0:
1182          continue
1183       group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1184       if x[1].has_key("subGroup") != 0:
1185          subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1186
1187    global SubGroupMap
1188    global GroupIDMap
1189    SubGroupMap = subgroup_map
1190    GroupIDMap = group_id_map
1191
1192 def generate_all(global_dir, ldap_conn):
1193    accounts = get_accounts(ldap_conn)
1194    host_attrs = get_hosts(ldap_conn)
1195
1196    global_dir += '/'
1197    # Generate global things
1198    accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1199
1200    accounts = filter(lambda x: not IsRetired(x), accounts)
1201
1202    CheckForward(accounts)
1203
1204    GenMailDisable(accounts, global_dir + "mail-disable")
1205    GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1206    GenDBM(accounts, global_dir + "mail-forward.db", 'emailForward')
1207    GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1208    GenDBM(accounts, global_dir + "mail-contentinspectionaction.db", 'mailContentInspectionAction')
1209    GenCDB(accounts, global_dir + "default-mail-options.cdb", 'mailDefaultOptions')
1210    GenDBM(accounts, global_dir + "default-mail-options.db", 'mailDefaultOptions')
1211    GenPrivate(accounts, global_dir + "debian-private")
1212    GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1213    GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1214    GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1215    GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1216    GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1217    GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1218    GenWebPassword(accounts, global_dir + "web-passwords")
1219    GenRtcPassword(accounts, global_dir + "rtc-passwords")
1220    GenTOTPSeed(accounts, global_dir + "users.oath")
1221    GenKeyrings(global_dir)
1222
1223    # Compatibility.
1224    GenForward(accounts, global_dir + "forward-alias")
1225
1226    GenAllUsers(accounts, global_dir + 'all-accounts.json')
1227    accounts = filter(lambda a: a not in accounts_disabled, accounts)
1228
1229    ssh_userkeys = GenSSHShadow(global_dir, accounts)
1230    GenMarkers(accounts, global_dir + "markers")
1231    GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1232    GenHosts(host_attrs, global_dir + "debianhosts")
1233
1234    GenDNS(accounts, global_dir + "dns-zone")
1235    GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1236
1237    setup_group_maps(ldap_conn)
1238
1239    for host in host_attrs:
1240       if "hostname" not in host[1]:
1241          continue
1242       generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1243
1244 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1245    current_host = host[1]['hostname'][0]
1246    OutDir = global_dir + current_host + '/'
1247    if not os.path.isdir(OutDir):
1248       os.mkdir(OutDir)
1249
1250    # Get the group list and convert any named groups to numerics
1251    GroupList = {}
1252    for groupname in AllowedGroupsPreload.strip().split(" "):
1253       GroupList[groupname] = True
1254    if 'allowedGroups' in host[1]:
1255       for groupname in host[1]['allowedGroups']:
1256          GroupList[groupname] = True
1257    for groupname in GroupList.keys():
1258       if groupname in GroupIDMap:
1259          GroupList[str(GroupIDMap[groupname])] = True
1260
1261    ExtraList = {}
1262    if 'exportOptions' in host[1]:
1263       for extra in host[1]['exportOptions']:
1264          ExtraList[extra.upper()] = True
1265
1266    if GroupList != {}:
1267       accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1268
1269    DoLink(global_dir, OutDir, "debianhosts")
1270    DoLink(global_dir, OutDir, "ssh_known_hosts")
1271    DoLink(global_dir, OutDir, "disabled-accounts")
1272
1273    sys.stdout.flush()
1274    if 'NOPASSWD' in ExtraList:
1275       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1276    else:
1277       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1278    sys.stdout.flush()
1279    grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1280    GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1281
1282    # Now we know who we're allowing on the machine, export
1283    # the relevant ssh keys
1284    GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1285
1286    if 'NOPASSWD' not in ExtraList:
1287       GenShadow(accounts, OutDir + "shadow")
1288
1289    # Link in global things
1290    if 'NOMARKERS' not in ExtraList:
1291       DoLink(global_dir, OutDir, "markers")
1292    DoLink(global_dir, OutDir, "mail-forward.cdb")
1293    DoLink(global_dir, OutDir, "mail-forward.db")
1294    DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1295    DoLink(global_dir, OutDir, "mail-contentinspectionaction.db")
1296    DoLink(global_dir, OutDir, "mail-disable")
1297    DoLink(global_dir, OutDir, "mail-greylist")
1298    DoLink(global_dir, OutDir, "mail-callout")
1299    DoLink(global_dir, OutDir, "mail-rbl")
1300    DoLink(global_dir, OutDir, "mail-rhsbl")
1301    DoLink(global_dir, OutDir, "mail-whitelist")
1302    DoLink(global_dir, OutDir, "all-accounts.json")
1303    DoLink(global_dir, Outdir, "default-mail-options.cdb")
1304    DoLink(global_dir, Outdir, "default-mail-options.db")
1305    GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1306    GenDBM(accounts, OutDir + "user-forward.db", 'emailForward')
1307    GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1308    GenDBM(accounts, OutDir + "batv-tokens.db", 'bATVToken')
1309
1310    # Compatibility.
1311    DoLink(global_dir, OutDir, "forward-alias")
1312
1313    if 'DNS' in ExtraList:
1314       DoLink(global_dir, OutDir, "dns-zone")
1315       DoLink(global_dir, OutDir, "dns-sshfp")
1316
1317    if 'AUTHKEYS' in ExtraList:
1318       DoLink(global_dir, OutDir, "authorized_keys")
1319
1320    if 'BSMTP' in ExtraList:
1321       GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1322
1323    if 'PRIVATE' in ExtraList:
1324       DoLink(global_dir, OutDir, "debian-private")
1325
1326    if 'GITOLITE' in ExtraList:
1327       GenSSHGitolite(all_accounts, all_hosts, OutDir + "ssh-gitolite", current_host=current_host)
1328    if 'exportOptions' in host[1]:
1329       for entry in host[1]['exportOptions']:
1330          v = entry.split('=',1)
1331          if v[0] != 'GITOLITE' or len(v) != 2: continue
1332          options = v[1].split(',')
1333          group = options.pop(0)
1334          gitolite_accounts = filter(lambda x: IsInGroup(x, [group], current_host), all_accounts)
1335          if 'nohosts' not in options:
1336             gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1337          else:
1338             gitolite_hosts = []
1339          command = None
1340          for opt in options:
1341             if opt.startswith('sshcmd='):
1342                command = opt.split('=',1)[1]
1343          GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(group,), sshcommand=command, current_host=current_host)
1344
1345    if 'WEB-PASSWORDS' in ExtraList:
1346       DoLink(global_dir, OutDir, "web-passwords")
1347
1348    if 'RTC-PASSWORDS' in ExtraList:
1349       DoLink(global_dir, OutDir, "rtc-passwords")
1350
1351    if 'TOTP' in ExtraList:
1352       DoLink(global_dir, OutDir, "users.oath")
1353
1354    if 'KEYRING' in ExtraList:
1355       for k in Keyrings:
1356          bn = os.path.basename(k)
1357          if os.path.isdir(k):
1358             src = os.path.join(global_dir, bn)
1359             replaceTree(src, OutDir)
1360          else:
1361             DoLink(global_dir, OutDir, bn)
1362    else:
1363       for k in Keyrings:
1364          try:
1365             bn = os.path.basename(k)
1366             target = os.path.join(OutDir, bn)
1367             if os.path.isdir(target):
1368                safe_rmtree(dst)
1369             else:
1370                posix.remove(target)
1371          except:
1372             pass
1373    DoLink(global_dir, OutDir, "last_update.trace")
1374
1375
1376 def getLastLDAPChangeTime(l):
1377    mods = l.search_s('cn=log',
1378          ldap.SCOPE_ONELEVEL,
1379          '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1380          ['reqEnd'])
1381
1382    last = 0
1383
1384    # Sort the list by reqEnd
1385    sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1386    # Take the last element in the array
1387    last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1388
1389    return last
1390
1391 def getLastKeyringChangeTime():
1392    krmod = 0
1393    for k in Keyrings:
1394       mt = os.path.getmtime(k)
1395       if mt > krmod:
1396          krmod = mt
1397
1398    return int(krmod)
1399
1400 def getLastBuildTime(gdir):
1401    cache_last_ldap_mod = 0
1402    cache_last_unix_mod = 0
1403    cache_last_run = 0
1404
1405    try:
1406       fd = open(os.path.join(gdir, "last_update.trace"), "r")
1407       cache_last_mod=fd.read().split()
1408       try:
1409          cache_last_ldap_mod = cache_last_mod[0]
1410          cache_last_unix_mod = int(cache_last_mod[1])
1411          cache_last_run = int(cache_last_mod[2])
1412       except IndexError, ValueError:
1413          pass
1414       fd.close()
1415    except IOError, e:
1416       if e.errno == errno.ENOENT:
1417          pass
1418       else:
1419          raise e
1420
1421    return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1422
1423 def mq_notify(options, message):
1424    options.section = 'dsa-udgenerate'
1425    options.config = '/etc/dsa/pubsub.conf'
1426
1427    config = Config(options)
1428    conf = {
1429       'rabbit_userid': config.username,
1430       'rabbit_password': config.password,
1431       'rabbit_virtual_host': config.vhost,
1432       'rabbit_hosts': ['pubsub02.debian.org', 'pubsub01.debian.org'],
1433       'use_ssl': False
1434    }
1435
1436    msg = {
1437       'message': message,
1438       'timestamp': int(time.time())
1439    }
1440    conn = None
1441    try:
1442       conn = Connection(conf=conf)
1443       conn.topic_send(config.topic,
1444             json.dumps(msg),
1445             exchange_name=config.exchange,
1446             timeout=5)
1447    finally:
1448       if conn:
1449          conn.close()
1450
1451 def ud_generate():
1452    parser = optparse.OptionParser()
1453    parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1454      help="Output directory.")
1455    parser.add_option("-f", "--force", dest="force", action="store_true",
1456      help="Force generation, even if no update to LDAP has happened.")
1457
1458    (options, args) = parser.parse_args()
1459    if len(args) > 0:
1460       parser.print_help()
1461       sys.exit(1)
1462
1463    if options.generatedir is not None:
1464       generate_dir = os.environ['UD_GENERATEDIR']
1465    elif 'UD_GENERATEDIR' in os.environ:
1466       generate_dir = os.environ['UD_GENERATEDIR']
1467    else:
1468       generate_dir = GenerateDir
1469
1470
1471    lockf = os.path.join(generate_dir, 'ud-generate.lock')
1472    lock = get_lock( lockf )
1473    if lock is None:
1474       sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1475       sys.exit(1)
1476
1477    l = make_ldap_conn()
1478
1479    time_started = int(time.time())
1480    ldap_last_mod = getLastLDAPChangeTime(l)
1481    unix_last_mod = getLastKeyringChangeTime()
1482    cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1483
1484    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)
1485
1486    fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1487    if need_update or options.force:
1488       msg = 'Update forced' if options.force else 'Update needed'
1489       generate_all(generate_dir, l)
1490       if use_mq:
1491          mq_notify(options, msg)
1492       last_run = int(time.time())
1493    fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1494    fd.close()
1495    sys.exit(0)
1496
1497
1498 if __name__ == "__main__":
1499    if 'UD_PROFILE' in os.environ:
1500       import cProfile
1501       import pstats
1502       cProfile.run('ud_generate()', "udg_prof")
1503       p = pstats.Stats('udg_prof')
1504       ##p.sort_stats('time').print_stats()
1505       p.sort_stats('cumulative').print_stats()
1506    else:
1507       ud_generate()
1508
1509 # vim:set et:
1510 # vim:set ts=3:
1511 # vim:set shiftwidth=3: