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