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