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