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