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