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