Lock ud-generate process
[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, sha, 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)
95       if stat[ST_MTIME] < time.time() - max_age:
96          sys.stderr.write("Removing stale lock %s"%(fn))
97          os.unlink(fn)
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 def GenSSHtarballs(global_dir, userlist, SSHFiles, grouprevmap, target):
378    OldMask = os.umask(0077)
379    tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
380    os.umask(OldMask)
381    for f in userlist.keys():
382       if f not in SSHFiles:
383          continue
384       # If we're not exporting their primary group, don't export
385       # the key and warn
386       grname = None
387       if userlist[f] in grouprevmap.keys():
388          grname = grouprevmap[userlist[f]]
389       else:
390          try:
391             if int(userlist[f]) <= 100:
392                # In these cases, look it up in the normal way so we
393                # deal with cases where, for instance, users are in group
394                # users as their primary group.
395                grname = grp.getgrgid(userlist[f])[0]
396          except Exception, e:
397             pass
398
399       if grname is None:
400          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])
401          continue
402
403       to = tf.gettarinfo(os.path.join(global_dir, 'userkeys', f), f)
404       # These will only be used where the username doesn't
405       # exist on the target system for some reason; hence,
406       # in those cases, the safest thing is for the file to
407       # be owned by root but group nobody.  This deals with
408       # the bloody obscure case where the group fails to exist
409       # whilst the user does (in which case we want to avoid
410       # ending up with a file which is owned user:root to avoid
411       # a fairly obvious attack vector)
412       to.uid = 0
413       to.gid = 65534
414       # Using the username / groupname fields avoids any need
415       # to give a shit^W^W^Wcare about the UIDoffset stuff.
416       to.uname = f
417       to.gname = grname
418       to.mode  = 0400
419
420       contents = file(os.path.join(global_dir, 'userkeys', f)).read()
421       lines = []
422       for line in contents.splitlines():
423          if line.startswith("allowed_hosts=") and ' ' in line:
424             machines, line = line.split('=', 1)[1].split(' ', 1)
425             if CurrentHost not in machines.split(','):
426                continue # skip this key
427          lines.append(line)
428       if not lines:
429          continue # no keys for this host
430       contents = "\n".join(lines) + "\n"
431       to.size = len(contents)
432       tf.addfile(to, StringIO(contents))
433
434    tf.close()
435    os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
436
437 # add a list of groups to existing groups,
438 # including all subgroups thereof, recursively.
439 # basically this proceduces the transitive hull of the groups in
440 # addgroups.
441 def addGroups(existingGroups, newGroups, uid):
442    for group in newGroups:
443       # if it's a <group>@host, split it and verify it's on the current host.
444       s = group.split('@', 1)
445       if len(s) == 2 and s[1] != CurrentHost:
446          continue
447       group = s[0]
448
449       # let's see if we handled this group already
450       if group in existingGroups:
451          continue
452
453       if not GroupIDMap.has_key(group):
454          print "Group", group, "does not exist but", uid, "is in it"
455          continue
456
457       existingGroups.append(group)
458
459       if SubGroupMap.has_key(group):
460          addGroups(existingGroups, SubGroupMap[group], uid)
461
462 # Generate the group list
463 def GenGroup(accounts, File):
464    grouprevmap = {}
465    F = None
466    try:
467       F = open(File + ".tdb.tmp", "w")
468      
469       # Generate the GroupMap
470       GroupMap = {}
471       for x in GroupIDMap.keys():
472          GroupMap[x] = []
473       GroupHasPrimaryMembers = {}
474
475       # Sort them into a list of groups having a set of users
476       for a in accounts:
477          GroupHasPrimaryMembers[ a['gidNumber'] ] = True
478          if not IsInGroup(a): continue
479          if not 'supplementaryGid' in a: continue
480
481          supgroups=[]
482          addGroups(supgroups, a['supplementaryGid'], a['uid'])
483          for g in supgroups:
484             GroupMap[g].append(a['uid'])
485
486       # Output the group file.
487       J = 0
488       for x in GroupMap.keys():
489          if GroupIDMap.has_key(x) == 0:
490             continue
491
492          if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
493             continue
494
495          grouprevmap[GroupIDMap[x]] = x
496
497          Line = "%s:x:%u:" % (x, GroupIDMap[x])
498          Comma = ''
499          for I in GroupMap[x]:
500             Line = Line + ("%s%s" % (Comma, I))
501             Comma = ','
502          Line = Sanitize(Line) + "\n"
503          F.write("0%u %s" % (J, Line))
504          F.write(".%s %s" % (x, Line))
505          F.write("=%u %s" % (GroupIDMap[x], Line))
506          J = J + 1
507   
508    # Oops, something unspeakable happened.
509    except:
510       Die(File, None, F)
511       raise
512    Done(File, None, F)
513   
514    return grouprevmap
515
516 def CheckForward(accounts):
517    for a in accounts:
518       if not 'emailForward' in a: continue
519
520
521       delete = False
522
523       if not IsInGroup(a): delete = True
524       # Do not allow people to try to buffer overflow busted parsers
525       elif len(a['emailForward']) > 200: delete = True
526       # Check the forwarding address
527       elif EmailCheck.match(a['emailForward']) is None: delete = True
528
529       if delete:
530          a.delete_mailforward()
531
532 # Generate the email forwarding list
533 def GenForward(accounts, File):
534    F = None
535    try:
536       OldMask = os.umask(0022)
537       F = open(File + ".tmp", "w", 0644)
538       os.umask(OldMask)
539
540       for a in accounts:
541          if not 'emailForward' in a: continue
542          Line = "%s: %s" % (a['uid'], a['emailForward'])
543          Line = Sanitize(Line) + "\n"
544          F.write(Line)
545
546    # Oops, something unspeakable happened.
547    except:
548       Die(File, F, None)
549       raise
550    Done(File, F, None)
551
552 def GenCDB(accounts, File, key):
553    Fdb = None
554    try:
555       OldMask = os.umask(0022)
556       Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
557       os.umask(OldMask)
558
559       # Write out the email address for each user
560       for a in accounts:
561          if not key in a: continue
562          value = a[key]
563          user = a['uid']
564          Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
565
566       Fdb.write("\n")
567    # Oops, something unspeakable happened.
568    except:
569       Fdb.close()
570       raise
571    if Fdb.close() != None:
572       raise "cdbmake gave an error"
573
574 # Generate the anon XEarth marker file
575 def GenMarkers(accounts, File):
576    F = None
577    try:
578       F = open(File + ".tmp", "w")
579
580       # Write out the position for each user
581       for a in accounts:
582          if not ('latitude' in a and 'longitude' in a): continue
583          try:
584             Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
585             Line = Sanitize(Line) + "\n"
586             F.write(Line)
587          except:
588             pass
589   
590    # Oops, something unspeakable happened.
591    except:
592       Die(File, F, None)
593       raise
594    Done(File, F, None)
595
596 # Generate the debian-private subscription list
597 def GenPrivate(accounts, File):
598    F = None
599    try:
600       F = open(File + ".tmp", "w")
601
602       # Write out the position for each user
603       for a in accounts:
604          if not a.is_active_user(): continue
605          if not 'privateSub' in a: continue
606          try:
607             Line = "%s"%(a['privateSub'])
608             Line = Sanitize(Line) + "\n"
609             F.write(Line)
610          except:
611             pass
612   
613    # Oops, something unspeakable happened.
614    except:
615       Die(File, F, None)
616       raise
617    Done(File, F, None)
618
619 # Generate a list of locked accounts
620 def GenDisabledAccounts(accounts, File):
621    F = None
622    try:
623       F = open(File + ".tmp", "w")
624       disabled_accounts = []
625
626       # Fetch all the users
627       for a in accounts:
628          if a.pw_active(): continue
629          Line = "%s:%s" % (a['uid'], "Account is locked")
630          disabled_accounts.append(a)
631          F.write(Sanitize(Line) + "\n")
632
633    # Oops, something unspeakable happened.
634    except:
635       Die(File, F, None)
636       raise
637    Done(File, F, None)
638    return disabled_accounts
639
640 # Generate the list of local addresses that refuse all mail
641 def GenMailDisable(accounts, File):
642    F = None
643    try:
644       F = open(File + ".tmp", "w")
645
646       for a in accounts:
647          if not 'mailDisableMessage' in a: continue
648          Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
649          Line = Sanitize(Line) + "\n"
650          F.write(Line)
651
652    # Oops, something unspeakable happened.
653    except:
654       Die(File, F, None)
655       raise
656    Done(File, F, None)
657
658 # Generate a list of uids that should have boolean affects applied
659 def GenMailBool(accounts, File, key):
660    F = None
661    try:
662       F = open(File + ".tmp", "w")
663
664       for a in accounts:
665          if not key in a: continue
666          if not a[key] == 'TRUE': continue
667          Line = "%s"%(a['uid'])
668          Line = Sanitize(Line) + "\n"
669          F.write(Line)
670
671    # Oops, something unspeakable happened.
672    except:
673       Die(File, F, None)
674       raise
675    Done(File, F, None)
676
677 # Generate a list of hosts for RBL or whitelist purposes.
678 def GenMailList(accounts, File, key):
679    F = None
680    try:
681       F = open(File + ".tmp", "w")
682
683       if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
684       else:                      validregex = re.compile('^[-\w.]+$')
685
686       for a in accounts:
687          if not key in a: continue
688
689          filtered = filter(lambda z: validregex.match(z), a[key])
690          if len(filtered) == 0: continue
691          if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
692          line = a['uid'] + ': ' + ' : '.join(filtered)
693          line = Sanitize(line) + "\n"
694          F.write(line)
695
696    # Oops, something unspeakable happened.
697    except:
698       Die(File, F, None)
699       raise
700    Done(File, F, None)
701
702 def isRoleAccount(account):
703    return 'debianRoleAccount' in account['objectClass']
704
705 # Generate the DNS Zone file
706 def GenDNS(accounts, File):
707    F = None
708    try:
709       F = open(File + ".tmp", "w")
710
711       # Fetch all the users
712       RRs = {}
713
714       # Write out the zone file entry for each user
715       for a in accounts:
716          if not 'dnsZoneEntry' in a: continue
717          if not a.is_active_user() and not isRoleAccount(a): continue
718
719          try:
720             F.write("; %s\n"%(a.email_address()))
721             for z in a["dnsZoneEntry"]:
722                Split = z.lower().split()
723                if Split[1].lower() == 'in':
724                   for y in range(0, len(Split)):
725                      if Split[y] == "$":
726                         Split[y] = "\n\t"
727                   Line = " ".join(Split) + "\n"
728                   F.write(Line)
729      
730                   Host = Split[0] + DNSZone
731                   if BSMTPCheck.match(Line) != None:
732                      F.write("; Has BSMTP\n")
733      
734                   # Write some identification information
735                   if not RRs.has_key(Host):
736                      if Split[2].lower() in ["a", "aaaa"]:
737                         Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
738                         for y in a["keyFingerPrint"]:
739                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
740                            F.write(Line)
741                         RRs[Host] = 1
742                else:
743                   Line = "; Err %s"%(str(Split))
744                   F.write(Line)
745      
746             F.write("\n")
747          except Exception, e:
748             F.write("; Errors:\n")
749             for line in str(e).split("\n"):
750                F.write("; %s\n"%(line))
751             pass
752   
753    # Oops, something unspeakable happened.
754    except:
755       Die(File, F, None)
756       raise
757    Done(File, F, None)
758
759 def ExtractDNSInfo(x):
760
761    TTLprefix="\t"
762    if 'dnsTTL' in x[1]:
763       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
764
765    DNSInfo = []
766    if x[1].has_key("ipHostNumber"):
767       for I in x[1]["ipHostNumber"]:
768          if IsV6Addr.match(I) != None:
769             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
770          else:
771             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
772
773    Algorithm = None
774
775    if 'sshRSAHostKey' in x[1]:
776       for I in x[1]["sshRSAHostKey"]:
777          Split = I.split()
778          if Split[0] == 'ssh-rsa':
779             Algorithm = 1
780          if Split[0] == 'ssh-dss':
781             Algorithm = 2
782          if Algorithm == None:
783             continue
784          Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
785          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
786
787    if 'architecture' in x[1]:
788       Arch = GetAttr(x, "architecture")
789       Mach = ""
790       if x[1].has_key("machine"):
791          Mach = " " + GetAttr(x, "machine")
792       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
793
794    if x[1].has_key("mXRecord"):
795       for I in x[1]["mXRecord"]:
796          DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
797
798    return DNSInfo
799
800 # Generate the DNS records
801 def GenZoneRecords(host_attrs, File):
802    F = None
803    try:
804       F = open(File + ".tmp", "w")
805
806       # Fetch all the hosts
807       for x in host_attrs:
808          if x[1].has_key("hostname") == 0:
809             continue
810
811          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
812             continue
813
814          DNSInfo = ExtractDNSInfo(x)
815          start = True
816          for Line in DNSInfo:
817             if start == True:
818                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
819                start = False
820             else:
821                Line = "\t\t\t%s" % (Line)
822
823             F.write(Line + "\n")
824
825         # this would write sshfp lines for services on machines
826         # but we can't yet, since some are cnames and we'll make
827         # an invalid zonefile
828         #
829         # for i in x[1].get("purpose", []):
830         #    m = PurposeHostField.match(i)
831         #    if m:
832         #       m = m.group(1)
833         #       # we ignore [[*..]] entries
834         #       if m.startswith('*'):
835         #          continue
836         #       if m.startswith('-'):
837         #          m = m[1:]
838         #       if m:
839         #          if not m.endswith(HostDomain):
840         #             continue
841         #          if not m.endswith('.'):
842         #             m = m + "."
843         #          for Line in DNSInfo:
844         #             if isSSHFP.match(Line):
845         #                Line = "%s\t%s" % (m, Line)
846         #                F.write(Line + "\n")
847
848    # Oops, something unspeakable happened.
849    except:
850       Die(File, F, None)
851       raise
852    Done(File, F, None)
853
854 # Generate the BSMTP file
855 def GenBSMTP(accounts, File, HomePrefix):
856    F = None
857    try:
858       F = open(File + ".tmp", "w")
859      
860       # Write out the zone file entry for each user
861       for a in accounts:
862          if not 'dnsZoneEntry' in a: continue
863          if not a.is_active_user(): continue
864
865          try:
866             for z in a["dnsZoneEntry"]:
867                Split = z.lower().split()
868                if Split[1].lower() == 'in':
869                   for y in range(0, len(Split)):
870                      if Split[y] == "$":
871                         Split[y] = "\n\t"
872                   Line = " ".join(Split) + "\n"
873      
874                   Host = Split[0] + DNSZone
875                   if BSMTPCheck.match(Line) != None:
876                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
877                                   a['uid'], HomePrefix, a['uid'], Host))
878      
879          except:
880             F.write("; Errors\n")
881             pass
882   
883    # Oops, something unspeakable happened.
884    except:
885       Die(File, F, None)
886       raise
887    Done(File, F, None)
888   
889 def HostToIP(Host, mapped=True):
890
891    IPAdresses = []
892
893    if Host[1].has_key("ipHostNumber"):
894       for addr in Host[1]["ipHostNumber"]:
895          IPAdresses.append(addr)
896          if IsV6Addr.match(addr) is None and mapped == "True":
897             IPAdresses.append("::ffff:"+addr)
898
899    return IPAdresses
900
901 # Generate the ssh known hosts file
902 def GenSSHKnown(host_attrs, File, mode=None):
903    F = None
904    try:
905       OldMask = os.umask(0022)
906       F = open(File + ".tmp", "w", 0644)
907       os.umask(OldMask)
908      
909       for x in host_attrs:
910          if x[1].has_key("hostname") == 0 or \
911             x[1].has_key("sshRSAHostKey") == 0:
912             continue
913          Host = GetAttr(x, "hostname")
914          HostNames = [ Host ]
915          if Host.endswith(HostDomain):
916             HostNames.append(Host[:-(len(HostDomain) + 1)])
917      
918          # in the purpose field [[host|some other text]] (where some other text is optional)
919          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
920          # file.  But so that we don't have to add everything we link we can add an asterisk
921          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
922          # http linking it we also support [[-hostname]] entries.
923          for i in x[1].get("purpose", []):
924             m = PurposeHostField.match(i)
925             if m:
926                m = m.group(1)
927                # we ignore [[*..]] entries
928                if m.startswith('*'):
929                   continue
930                if m.startswith('-'):
931                   m = m[1:]
932                if m:
933                   HostNames.append(m)
934                   if m.endswith(HostDomain):
935                      HostNames.append(m[:-(len(HostDomain) + 1)])
936      
937          for I in x[1]["sshRSAHostKey"]:
938             if mode and mode == 'authorized_keys':
939                hosts = HostToIP(x)
940                if 'sshdistAuthKeysHost' in x[1]:
941                   hosts += x[1]['sshdistAuthKeysHost']
942                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)
943             else:
944                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
945             Line = Sanitize(Line) + "\n"
946             F.write(Line)
947    # Oops, something unspeakable happened.
948    except:
949       Die(File, F, None)
950       raise
951    Done(File, F, None)
952
953 # Generate the debianhosts file (list of all IP addresses)
954 def GenHosts(host_attrs, File):
955    F = None
956    try:
957       OldMask = os.umask(0022)
958       F = open(File + ".tmp", "w", 0644)
959       os.umask(OldMask)
960      
961       seen = set()
962
963       for x in host_attrs:
964
965          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
966             continue
967
968          if not 'ipHostNumber' in x[1]:
969             continue
970
971          addrs = x[1]["ipHostNumber"]
972          for addr in addrs:
973             if addr not in seen:
974                seen.add(addr)
975                addr = Sanitize(addr) + "\n"
976                F.write(addr)
977
978    # Oops, something unspeakable happened.
979    except:
980       Die(File, F, None)
981       raise
982    Done(File, F, None)
983
984 def GenKeyrings(OutDir):
985    for k in Keyrings:
986       shutil.copy(k, OutDir)
987
988
989 def get_accounts(ldap_conn):
990    # Fetch all the users
991    passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0)))",\
992                    ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
993                     "gecos", "loginShell", "userPassword", "shadowLastChange",\
994                     "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
995                     "shadowExpire", "emailForward", "latitude", "longitude",\
996                     "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
997                     "keyFingerPrint", "privateSub", "mailDisableMessage",\
998                     "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
999                     "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1000                     "mailContentInspectionAction"])
1001
1002    if passwd_attrs is None:
1003       raise UDEmptyList, "No Users"
1004    accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1005    accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1006
1007    return accounts
1008
1009 def get_hosts(ldap_conn):
1010    # Fetch all the hosts
1011    HostAttrs    = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1012                    ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1013                     "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1014
1015    if HostAttrs == None:
1016       raise UDEmptyList, "No Hosts"
1017
1018    HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1019
1020    return HostAttrs
1021
1022
1023 def make_ldap_conn():
1024    # Connect to the ldap server
1025    l = connectLDAP()
1026    # for testing purposes it's sometimes useful to pass username/password
1027    # via the environment
1028    if 'UD_CREDENTIALS' in os.environ:
1029       Pass = os.environ['UD_CREDENTIALS'].split()
1030    else:
1031       F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1032       Pass = F.readline().strip().split(" ")
1033       F.close()
1034    l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1035
1036    return l
1037
1038 def generate_all(global_dir, ldap_conn):
1039    accounts = get_accounts(ldap_conn)
1040    host_attrs = get_hosts(ldap_conn)
1041
1042    global_dir += '/'
1043    # Generate global things
1044    accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1045
1046    accounts = filter(lambda x: not IsRetired(x), accounts)
1047    #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1048
1049    CheckForward(accounts)
1050
1051    GenMailDisable(accounts, global_dir + "mail-disable")
1052    GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1053    GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1054    GenPrivate(accounts, global_dir + "debian-private")
1055    GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1056    GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1057    GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1058    GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1059    GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1060    GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1061    GenKeyrings(global_dir)
1062
1063    # Compatibility.
1064    GenForward(accounts, global_dir + "forward-alias")
1065
1066    GenAllUsers(accounts, global_dir + 'all-accounts.json')
1067    accounts = filter(lambda a: not a in accounts_disabled, accounts)
1068
1069    ssh_files = GenSSHShadow(global_dir, accounts)
1070    GenMarkers(accounts, global_dir + "markers")
1071    GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1072    GenHosts(host_attrs, global_dir + "debianhosts")
1073
1074    GenDNS(accounts, global_dir + "dns-zone")
1075    GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1076
1077    for host in host_attrs:
1078       if not "hostname" in host[1]:
1079          continue
1080       generate_host(host, global_dir, accounts, ssh_files)
1081
1082 def generate_host(host, global_dir, accounts, ssh_files):
1083    global CurrentHost
1084
1085    CurrentHost = host[1]['hostname'][0]
1086    OutDir = global_dir + CurrentHost + '/'
1087    try:
1088       os.mkdir(OutDir)
1089    except:
1090       pass
1091
1092    # Get the group list and convert any named groups to numerics
1093    GroupList = {}
1094    for groupname in AllowedGroupsPreload.strip().split(" "):
1095       GroupList[groupname] = True
1096    if 'allowedGroups' in host[1]:
1097       for groupname in host[1]['allowedGroups']:
1098          GroupList[groupname] = True
1099    for groupname in GroupList.keys():
1100       if groupname in GroupIDMap:
1101          GroupList[str(GroupIDMap[groupname])] = True
1102
1103    ExtraList = {}
1104    if 'exportOptions' in host[1]:
1105       for extra in host[1]['exportOptions']:
1106          ExtraList[extra.upper()] = True
1107
1108    global Allowed
1109    Allowed = GroupList
1110    if Allowed == {}:
1111       Allowed = None
1112
1113    DoLink(global_dir, OutDir, "debianhosts")
1114    DoLink(global_dir, OutDir, "ssh_known_hosts")
1115    DoLink(global_dir, OutDir, "disabled-accounts")
1116
1117    sys.stdout.flush()
1118    if 'NOPASSWD' in ExtraList:
1119       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1120    else:
1121       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1122    sys.stdout.flush()
1123    grouprevmap = GenGroup(accounts, OutDir + "group")
1124    GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1125
1126    # Now we know who we're allowing on the machine, export
1127    # the relevant ssh keys
1128    GenSSHtarballs(global_dir, userlist, ssh_files, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1129
1130    if not 'NOPASSWD' in ExtraList:
1131       GenShadow(accounts, OutDir + "shadow")
1132
1133    # Link in global things
1134    if not 'NOMARKERS' in ExtraList:
1135       DoLink(global_dir, OutDir, "markers")
1136    DoLink(global_dir, OutDir, "mail-forward.cdb")
1137    DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1138    DoLink(global_dir, OutDir, "mail-disable")
1139    DoLink(global_dir, OutDir, "mail-greylist")
1140    DoLink(global_dir, OutDir, "mail-callout")
1141    DoLink(global_dir, OutDir, "mail-rbl")
1142    DoLink(global_dir, OutDir, "mail-rhsbl")
1143    DoLink(global_dir, OutDir, "mail-whitelist")
1144    DoLink(global_dir, OutDir, "all-accounts.json")
1145    GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "user-forward.cdb", 'emailForward')
1146    GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "batv-tokens.cdb", 'bATVToken')
1147    GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1148
1149    # Compatibility.
1150    DoLink(global_dir, OutDir, "forward-alias")
1151
1152    if 'DNS' in ExtraList:
1153       DoLink(global_dir, OutDir, "dns-zone")
1154       DoLink(global_dir, OutDir, "dns-sshfp")
1155
1156    if 'AUTHKEYS' in ExtraList:
1157       DoLink(global_dir, OutDir, "authorized_keys")
1158
1159    if 'BSMTP' in ExtraList:
1160       GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1161
1162    if 'PRIVATE' in ExtraList:
1163       DoLink(global_dir, OutDir, "debian-private")
1164
1165    if 'KEYRING' in ExtraList:
1166       for k in Keyrings:
1167         DoLink(global_dir, OutDir, os.path.basename(k))
1168    else:
1169       for k in Keyrings:
1170          try:
1171             posix.remove(OutDir + os.path.basename(k))
1172          except:
1173             pass
1174
1175 l = make_ldap_conn()
1176
1177 # Fetch all the groups
1178 GroupIDMap = {}
1179 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1180                   ["gid", "gidNumber", "subGroup"])
1181
1182 # Generate the SubGroupMap and GroupIDMap
1183 for x in attrs:
1184    if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1185       continue
1186    if x[1].has_key("gidNumber") == 0:
1187       continue
1188    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1189    if x[1].has_key("subGroup") != 0:
1190       SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1191
1192 # override globaldir for testing
1193 if 'UD_GENERATEDIR' in os.environ:
1194    GenerateDir = os.environ['UD_GENERATEDIR']
1195
1196 try:
1197    lock = get_lock( os.path.join(GenerateDir, 'ud-generate.lock') )
1198    if lock is None:
1199       sys.stderr.write("Could not acquire lock %s.\n"%(fn))
1200       sys.exit(1)
1201
1202    generate_all(GenerateDir, l)
1203
1204 finally:
1205    if not lock is None:
1206       lock.release()
1207
1208 # vim:set et:
1209 # vim:set ts=3:
1210 # vim:set shiftwidth=3: