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