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