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