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