A set of copyright headers
[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 #      global HostAttrs
782 #
783 #      for x in HostAttrs:
784 #         if x[1].has_key("hostname") == 0 or \
785 #            x[1].has_key("architecture") == 0 or\
786 #            x[1].has_key("sshRSAHostKey") == 0:
787 #            continue
788 #
789 #         if IsDebianHost.match(GetAttr(x, "hostname")) is not None:
790 #            continue
791 #
792 #         DNSInfo = ExtractDNSInfo(x)
793 #         start = True
794 #         for Line in DNSInfo:
795 #            if start == True:
796 #               Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
797 #               start = False
798 #            else:
799 #               Line = "\t\t\t%s" % (Line)
800 #            F.write(Line + "\n")
801
802       # Fetch all the users
803       global PasswdAttrs
804      
805       # Write out the zone file entry for each user
806       for x in PasswdAttrs:
807          if x[1].has_key("dnsZoneEntry") == 0:
808             continue
809      
810          # If the account has no PGP key, do not write it
811          if x[1].has_key("keyFingerPrint") == 0 and not isRoleAccount(x[1]):
812             continue
813          try:
814             F.write("; %s\n"%(EmailAddress(x)))
815             for z in x[1]["dnsZoneEntry"]:
816                Split = z.lower().split()
817                if Split[1].lower() == 'in':
818                   for y in range(0, len(Split)):
819                      if Split[y] == "$":
820                         Split[y] = "\n\t"
821                   Line = " ".join(Split) + "\n"
822                   F.write(Line)
823      
824                   Host = Split[0] + DNSZone
825                   if BSMTPCheck.match(Line) != None:
826                      F.write("; Has BSMTP\n")
827      
828                   # Write some identification information
829                   if Split[2].lower() == "a":
830                      Line = "%s IN TXT \"%s\"\n"%(Split[0], EmailAddress(x))
831                      for y in x[1]["keyFingerPrint"]:
832                         Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
833                      F.write(Line)
834                else:
835                   Line = "; Err %s"%(str(Split))
836                   F.write(Line)
837      
838             F.write("\n")
839          except:
840             F.write("; Errors\n")
841             pass
842   
843    # Oops, something unspeakable happened.
844    except:
845       Die(File, F, None)
846       raise
847    Done(File, F, None)
848
849 def ExtractDNSInfo(x):
850
851    TTLprefix="\t"
852    if 'dnsTTL' in x[1]:
853       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
854
855    DNSInfo = []
856    if x[1].has_key("ipHostNumber"):
857       for I in x[1]["ipHostNumber"]:
858          if IsV6Addr.match(I) != None:
859             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
860          else:
861             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
862
863    Algorithm = None
864
865    if 'sshRSAHostKey' in x[1]:
866       for I in x[1]["sshRSAHostKey"]:
867          Split = I.split()
868          if Split[0] == 'ssh-rsa':
869             Algorithm = 1
870          if Split[0] == 'ssh-dss':
871             Algorithm = 2
872          if Algorithm == None:
873             continue
874          Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
875          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
876
877    if 'architecture' in x[1]:
878       Arch = GetAttr(x, "architecture")
879       Mach = ""
880       if x[1].has_key("machine"):
881          Mach = " " + GetAttr(x, "machine")
882       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
883
884    if x[1].has_key("mXRecord"):
885       for I in x[1]["mXRecord"]:
886          DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
887
888    return DNSInfo
889
890 # Generate the DNS records
891 def GenZoneRecords(File):
892    F = None
893    try:
894       F = open(File + ".tmp", "w")
895
896       # Fetch all the hosts
897       global HostAttrs
898
899       for x in HostAttrs:
900          if x[1].has_key("hostname") == 0:
901             continue
902
903          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
904             continue
905
906          DNSInfo = ExtractDNSInfo(x)
907          start = True
908          for Line in DNSInfo:
909             if start == True:
910                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
911                start = False
912             else:
913                Line = "\t\t\t%s" % (Line)
914
915             F.write(Line + "\n")
916
917    # Oops, something unspeakable happened.
918    except:
919       Die(File, F, None)
920       raise
921    Done(File, F, None)
922
923 # Generate the BSMTP file
924 def GenBSMTP(File, HomePrefix):
925    F = None
926    try:
927       F = open(File + ".tmp", "w")
928      
929       # Fetch all the users
930       global PasswdAttrs
931      
932       # Write out the zone file entry for each user
933       for x in PasswdAttrs:
934          if x[1].has_key("dnsZoneEntry") == 0:
935             continue
936      
937          # If the account has no PGP key, do not write it
938          if x[1].has_key("keyFingerPrint") == 0:
939             continue
940          try:
941             for z in x[1]["dnsZoneEntry"]:
942                Split = z.lower().split()
943                if Split[1].lower() == 'in':
944                   for y in range(0, len(Split)):
945                      if Split[y] == "$":
946                         Split[y] = "\n\t"
947                   Line = " ".join(Split) + "\n"
948      
949                   Host = Split[0] + DNSZone
950                   if BSMTPCheck.match(Line) != None:
951                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
952                                   GetAttr(x, "uid"), HomePrefix, GetAttr(x, "uid"), Host))
953      
954          except:
955             F.write("; Errors\n")
956             pass
957   
958    # Oops, something unspeakable happened.
959    except:
960       Die(File, F, None)
961       raise
962    Done(File, F, None)
963   
964 def HostToIP(Host, mapped=True):
965
966    IPAdresses = []
967
968    if Host[1].has_key("ipHostNumber"):
969       for addr in Host[1]["ipHostNumber"]:
970          IPAdresses.append(addr)
971          if IsV6Addr.match(addr) is None and mapped == "True":
972             IPAdresses.append("::ffff:"+addr)
973
974    return IPAdresses
975
976 # Generate the ssh known hosts file
977 def GenSSHKnown(File, mode=None):
978    F = None
979    try:
980       OldMask = os.umask(0022)
981       F = open(File + ".tmp", "w", 0644)
982       os.umask(OldMask)
983      
984       global HostAttrs
985      
986       for x in HostAttrs:
987          if x[1].has_key("hostname") == 0 or \
988             x[1].has_key("sshRSAHostKey") == 0:
989             continue
990          Host = GetAttr(x, "hostname")
991          HostNames = [ Host ]
992          if Host.endswith(HostDomain):
993             HostNames.append(Host[:-(len(HostDomain) + 1)])
994      
995          # in the purpose field [[host|some other text]] (where some other text is optional)
996          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
997          # file.  But so that we don't have to add everything we link we can add an asterisk
998          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
999          # http linking it we also support [[-hostname]] entries.
1000          for i in x[1].get("purpose", []):
1001             m = PurposeHostField.match(i)
1002             if m:
1003                m = m.group(1)
1004                # we ignore [[*..]] entries
1005                if m.startswith('*'):
1006                   continue
1007                if m.startswith('-'):
1008                   m = m[1:]
1009                if m:
1010                   HostNames.append(m)
1011                   if m.endswith(HostDomain):
1012                      HostNames.append(m[:-(len(HostDomain) + 1)])
1013      
1014          for I in x[1]["sshRSAHostKey"]:
1015             if mode and mode == 'authorized_keys':
1016                hosts = HostToIP(x)
1017                if 'sshdistAuthKeysHost' in x[1]:
1018                   hosts += x[1]['sshdistAuthKeysHost']
1019                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)
1020                #Line = 'command="rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding %s' % (Host,I)
1021             else:
1022                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1023             Line = Sanitize(Line) + "\n"
1024             F.write(Line)
1025    # Oops, something unspeakable happened.
1026    except:
1027       Die(File, F, None)
1028       raise
1029    Done(File, F, None)
1030
1031 # Generate the debianhosts file (list of all IP addresses)
1032 def GenHosts(File):
1033    F = None
1034    try:
1035       OldMask = os.umask(0022)
1036       F = open(File + ".tmp", "w", 0644)
1037       os.umask(OldMask)
1038      
1039       seen = set()
1040
1041       global HostAttrs
1042
1043       for x in HostAttrs:
1044
1045          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1046             continue
1047
1048          if not 'ipHostNumber' in x[1]:
1049             continue
1050
1051          addrs = x[1]["ipHostNumber"]
1052          for addr in addrs:
1053             if addr not in seen:
1054                seen.add(addr)
1055                addr = Sanitize(addr) + "\n"
1056                F.write(addr)
1057
1058    # Oops, something unspeakable happened.
1059    except:
1060       Die(File, F, None)
1061       raise
1062    Done(File, F, None)
1063
1064 def GenKeyrings(OutDir):
1065    for k in Keyrings:
1066       shutil.copy(k, OutDir)
1067
1068 # Connect to the ldap server
1069 l = connectLDAP()
1070 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1071 Pass = F.readline().strip().split(" ")
1072 F.close()
1073 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1074
1075 # Fetch all the groups
1076 GroupIDMap = {}
1077 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1078                   ["gid", "gidNumber", "subGroup"])
1079
1080 # Generate the SubGroupMap and GroupIDMap
1081 for x in Attrs:
1082    if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1083       continue
1084    if x[1].has_key("gidNumber") == 0:
1085       continue
1086    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1087    if x[1].has_key("subGroup") != 0:
1088       SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1089
1090 # Fetch all the users
1091 PasswdAttrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "uid=*",\
1092                 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1093                  "gecos", "loginShell", "userPassword", "shadowLastChange",\
1094                  "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1095                  "shadowExpire", "emailForward", "latitude", "longitude",\
1096                  "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1097                  "keyFingerPrint", "privateSub", "mailDisableMessage",\
1098                  "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1099                  "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1100                  "mailContentInspectionAction"])
1101
1102 if PasswdAttrs is None:
1103    raise UDEmptyList, "No Users"
1104
1105 PasswdAttrs.sort(lambda x, y: cmp((GetAttr(x, "uid")).lower(), (GetAttr(y, "uid")).lower()))
1106
1107 # Fetch all the hosts
1108 HostAttrs    = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1109                 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1110                  "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1111
1112 if HostAttrs == None:
1113    raise UDEmptyList, "No Hosts"
1114
1115 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1116
1117 # Generate global things
1118 GlobalDir = GenerateDir + "/"
1119 GenDisabledAccounts(GlobalDir + "disabled-accounts")
1120
1121 PasswdAttrs = filter(lambda x: not IsRetired(x), PasswdAttrs)
1122 DebianDDUsers = filter(lambda x: IsGidDebian(x), PasswdAttrs)
1123
1124 CheckForward()
1125
1126 GenMailDisable(GlobalDir + "mail-disable")
1127 GenCDB(GlobalDir + "mail-forward.cdb", PasswdAttrs, 'emailForward')
1128 GenCDB(GlobalDir + "mail-contentinspectionaction.cdb", PasswdAttrs, 'mailContentInspectionAction')
1129 GenPrivate(GlobalDir + "debian-private")
1130 GenSSHKnown(GlobalDir+"authorized_keys", 'authorized_keys')
1131 GenMailBool(GlobalDir + "mail-greylist", "mailGreylisting")
1132 GenMailBool(GlobalDir + "mail-callout", "mailCallout")
1133 GenMailList(GlobalDir + "mail-rbl", "mailRBL")
1134 GenMailList(GlobalDir + "mail-rhsbl", "mailRHSBL")
1135 GenMailList(GlobalDir + "mail-whitelist", "mailWhitelist")
1136 GenKeyrings(GlobalDir)
1137
1138 # Compatibility.
1139 GenForward(GlobalDir + "forward-alias")
1140
1141 PasswdAttrs = filter(lambda x: not x in DisabledUsers, PasswdAttrs)
1142
1143 SSHFiles = GenSSHShadow()
1144 GenMarkers(GlobalDir + "markers")
1145 GenSSHKnown(GlobalDir + "ssh_known_hosts")
1146 GenHosts(GlobalDir + "debianhosts")
1147
1148 for host in HostAttrs:
1149    if not "hostname" in host[1]:
1150       continue
1151
1152    CurrentHost = host[1]['hostname'][0]
1153    OutDir = GenerateDir + '/' + CurrentHost + '/'
1154    try:
1155       os.mkdir(OutDir)
1156    except: 
1157       pass
1158
1159    # Get the group list and convert any named groups to numerics
1160    GroupList = {}
1161    for groupname in AllowedGroupsPreload.strip().split(" "):
1162       GroupList[groupname] = True
1163    if 'allowedGroups' in host[1]:
1164       for groupname in host[1]['allowedGroups']:
1165          GroupList[groupname] = True
1166    for groupname in GroupList.keys():
1167       if groupname in GroupIDMap:
1168          GroupList[str(GroupIDMap[groupname])] = True
1169
1170    ExtraList = {}
1171    if 'exportOptions' in host[1]:
1172       for extra in host[1]['exportOptions']:
1173          ExtraList[extra.upper()] = True
1174
1175    Allowed = GroupList
1176    if Allowed == {}:
1177       Allowed = None
1178
1179    DoLink(GlobalDir, OutDir, "debianhosts")
1180    DoLink(GlobalDir, OutDir, "ssh_known_hosts")
1181    DoLink(GlobalDir, OutDir, "disabled-accounts")
1182
1183    sys.stdout.flush()
1184    if 'NOPASSWD' in ExtraList:
1185       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "*")
1186    else:
1187       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "x")
1188    sys.stdout.flush()
1189    grouprevmap = GenGroup(OutDir + "group")
1190    GenShadowSudo(OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1191
1192    # Now we know who we're allowing on the machine, export
1193    # the relevant ssh keys
1194    GenSSHtarballs(userlist, SSHFiles, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1195
1196    if not 'NOPASSWD' in ExtraList:
1197       GenShadow(OutDir + "shadow")
1198
1199    # Link in global things
1200    if not 'NOMARKERS' in ExtraList:
1201       DoLink(GlobalDir, OutDir, "markers")
1202    DoLink(GlobalDir, OutDir, "mail-forward.cdb")
1203    DoLink(GlobalDir, OutDir, "mail-contentinspectionaction.cdb")
1204    DoLink(GlobalDir, OutDir, "mail-disable")
1205    DoLink(GlobalDir, OutDir, "mail-greylist")
1206    DoLink(GlobalDir, OutDir, "mail-callout")
1207    DoLink(GlobalDir, OutDir, "mail-rbl")
1208    DoLink(GlobalDir, OutDir, "mail-rhsbl")
1209    DoLink(GlobalDir, OutDir, "mail-whitelist")
1210    GenCDB(OutDir + "user-forward.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'emailForward')
1211    GenCDB(OutDir + "batv-tokens.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'bATVToken')
1212    GenCDB(OutDir + "default-mail-options.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'mailDefaultOptions')
1213
1214    # Compatibility.
1215    DoLink(GlobalDir, OutDir, "forward-alias")
1216
1217    if 'DNS' in ExtraList:
1218       GenDNS(OutDir + "dns-zone")
1219       GenZoneRecords(OutDir + "dns-sshfp")
1220
1221    if 'AUTHKEYS' in ExtraList:
1222       DoLink(GlobalDir, OutDir, "authorized_keys")
1223
1224    if 'BSMTP' in ExtraList:
1225       GenBSMTP(OutDir + "bsmtp", HomePrefix)
1226
1227    if 'PRIVATE' in ExtraList:
1228       DoLink(GlobalDir, OutDir, "debian-private")
1229
1230    if 'KEYRING' in ExtraList:
1231       for k in Keyrings:
1232         DoLink(GlobalDir, OutDir, os.path.basename(k))
1233    else:
1234       for k in Keyrings:
1235          try: 
1236             posix.remove(OutDir + os.path.basename(k))
1237          except:
1238             pass
1239
1240 # vim:set et:
1241 # vim:set ts=3:
1242 # vim:set shiftwidth=3: