And GenMailList
[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       if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
705       else:                      validregex = re.compile('^[-\w.]+$')
706
707       for x in PasswdAttrs:
708          a = UDLdap.Account(x[0], x[1])
709          if not key in a: continue
710
711          filtered = filter(lambda z: validregex.match(z), a[key])
712          if len(filtered) == 0: continue
713          if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
714          line = a['uid'] + ': ' + ' : '.join(filtered)
715          line = Sanitize(line) + "\n"
716          F.write(line)
717
718    # Oops, something unspeakable happened.
719    except:
720       Die(File, F, None)
721       raise
722    Done(File, F, None)
723
724 def isRoleAccount(pwEntry):
725    if not pwEntry.has_key("objectClass"):
726       raise "pwEntry has no objectClass"
727    oc =  pwEntry['objectClass']
728    try:
729       i = oc.index('debianRoleAccount')
730       return True
731    except ValueError:
732       return False
733
734 # Generate the DNS Zone file
735 def GenDNS(File):
736    F = None
737    try:
738       F = open(File + ".tmp", "w")
739      
740       # Fetch all the users
741       global PasswdAttrs
742       RRs = {}
743      
744       # Write out the zone file entry for each user
745       for x in PasswdAttrs:
746          if x[1].has_key("dnsZoneEntry") == 0:
747             continue
748      
749          # If the account has no PGP key, do not write it
750          if x[1].has_key("keyFingerPrint") == 0 and not isRoleAccount(x[1]):
751             continue
752          try:
753             F.write("; %s\n"%(EmailAddress(x)))
754             for z in x[1]["dnsZoneEntry"]:
755                Split = z.lower().split()
756                if Split[1].lower() == 'in':
757                   for y in range(0, len(Split)):
758                      if Split[y] == "$":
759                         Split[y] = "\n\t"
760                   Line = " ".join(Split) + "\n"
761                   F.write(Line)
762      
763                   Host = Split[0] + DNSZone
764                   if BSMTPCheck.match(Line) != None:
765                      F.write("; Has BSMTP\n")
766      
767                   # Write some identification information
768                   if not RRs.has_key(Host):
769                      if Split[2].lower() in ["a", "aaaa"]:
770                         Line = "%s IN TXT \"%s\"\n"%(Split[0], EmailAddress(x))
771                         for y in x[1]["keyFingerPrint"]:
772                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
773                            F.write(Line)
774                         RRs[Host] = 1
775                else:
776                   Line = "; Err %s"%(str(Split))
777                   F.write(Line)
778      
779             F.write("\n")
780          except:
781             F.write("; Errors\n")
782             pass
783   
784    # Oops, something unspeakable happened.
785    except:
786       Die(File, F, None)
787       raise
788    Done(File, F, None)
789
790 def ExtractDNSInfo(x):
791
792    TTLprefix="\t"
793    if 'dnsTTL' in x[1]:
794       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
795
796    DNSInfo = []
797    if x[1].has_key("ipHostNumber"):
798       for I in x[1]["ipHostNumber"]:
799          if IsV6Addr.match(I) != None:
800             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
801          else:
802             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
803
804    Algorithm = None
805
806    if 'sshRSAHostKey' in x[1]:
807       for I in x[1]["sshRSAHostKey"]:
808          Split = I.split()
809          if Split[0] == 'ssh-rsa':
810             Algorithm = 1
811          if Split[0] == 'ssh-dss':
812             Algorithm = 2
813          if Algorithm == None:
814             continue
815          Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
816          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
817
818    if 'architecture' in x[1]:
819       Arch = GetAttr(x, "architecture")
820       Mach = ""
821       if x[1].has_key("machine"):
822          Mach = " " + GetAttr(x, "machine")
823       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
824
825    if x[1].has_key("mXRecord"):
826       for I in x[1]["mXRecord"]:
827          DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
828
829    return DNSInfo
830
831 # Generate the DNS records
832 def GenZoneRecords(File):
833    F = None
834    try:
835       F = open(File + ".tmp", "w")
836
837       # Fetch all the hosts
838       global HostAttrs
839
840       for x in HostAttrs:
841          if x[1].has_key("hostname") == 0:
842             continue
843
844          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
845             continue
846
847          DNSInfo = ExtractDNSInfo(x)
848          start = True
849          for Line in DNSInfo:
850             if start == True:
851                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
852                start = False
853             else:
854                Line = "\t\t\t%s" % (Line)
855
856             F.write(Line + "\n")
857
858         # this would write sshfp lines for services on machines
859         # but we can't yet, since some are cnames and we'll make
860         # an invalid zonefile
861         #
862         # for i in x[1].get("purpose", []):
863         #    m = PurposeHostField.match(i)
864         #    if m:
865         #       m = m.group(1)
866         #       # we ignore [[*..]] entries
867         #       if m.startswith('*'):
868         #          continue
869         #       if m.startswith('-'):
870         #          m = m[1:]
871         #       if m:
872         #          if not m.endswith(HostDomain):
873         #             continue
874         #          if not m.endswith('.'):
875         #             m = m + "."
876         #          for Line in DNSInfo:
877         #             if isSSHFP.match(Line):
878         #                Line = "%s\t%s" % (m, Line)
879         #                F.write(Line + "\n")
880
881    # Oops, something unspeakable happened.
882    except:
883       Die(File, F, None)
884       raise
885    Done(File, F, None)
886
887 # Generate the BSMTP file
888 def GenBSMTP(File, HomePrefix):
889    F = None
890    try:
891       F = open(File + ".tmp", "w")
892      
893       # Fetch all the users
894       global PasswdAttrs
895      
896       # Write out the zone file entry for each user
897       for x in PasswdAttrs:
898          if x[1].has_key("dnsZoneEntry") == 0:
899             continue
900      
901          # If the account has no PGP key, do not write it
902          if x[1].has_key("keyFingerPrint") == 0:
903             continue
904          try:
905             for z in x[1]["dnsZoneEntry"]:
906                Split = z.lower().split()
907                if Split[1].lower() == 'in':
908                   for y in range(0, len(Split)):
909                      if Split[y] == "$":
910                         Split[y] = "\n\t"
911                   Line = " ".join(Split) + "\n"
912      
913                   Host = Split[0] + DNSZone
914                   if BSMTPCheck.match(Line) != None:
915                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
916                                   GetAttr(x, "uid"), HomePrefix, GetAttr(x, "uid"), Host))
917      
918          except:
919             F.write("; Errors\n")
920             pass
921   
922    # Oops, something unspeakable happened.
923    except:
924       Die(File, F, None)
925       raise
926    Done(File, F, None)
927   
928 def HostToIP(Host, mapped=True):
929
930    IPAdresses = []
931
932    if Host[1].has_key("ipHostNumber"):
933       for addr in Host[1]["ipHostNumber"]:
934          IPAdresses.append(addr)
935          if IsV6Addr.match(addr) is None and mapped == "True":
936             IPAdresses.append("::ffff:"+addr)
937
938    return IPAdresses
939
940 # Generate the ssh known hosts file
941 def GenSSHKnown(File, mode=None):
942    F = None
943    try:
944       OldMask = os.umask(0022)
945       F = open(File + ".tmp", "w", 0644)
946       os.umask(OldMask)
947      
948       global HostAttrs
949      
950       for x in HostAttrs:
951          if x[1].has_key("hostname") == 0 or \
952             x[1].has_key("sshRSAHostKey") == 0:
953             continue
954          Host = GetAttr(x, "hostname")
955          HostNames = [ Host ]
956          if Host.endswith(HostDomain):
957             HostNames.append(Host[:-(len(HostDomain) + 1)])
958      
959          # in the purpose field [[host|some other text]] (where some other text is optional)
960          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
961          # file.  But so that we don't have to add everything we link we can add an asterisk
962          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
963          # http linking it we also support [[-hostname]] entries.
964          for i in x[1].get("purpose", []):
965             m = PurposeHostField.match(i)
966             if m:
967                m = m.group(1)
968                # we ignore [[*..]] entries
969                if m.startswith('*'):
970                   continue
971                if m.startswith('-'):
972                   m = m[1:]
973                if m:
974                   HostNames.append(m)
975                   if m.endswith(HostDomain):
976                      HostNames.append(m[:-(len(HostDomain) + 1)])
977      
978          for I in x[1]["sshRSAHostKey"]:
979             if mode and mode == 'authorized_keys':
980                hosts = HostToIP(x)
981                if 'sshdistAuthKeysHost' in x[1]:
982                   hosts += x[1]['sshdistAuthKeysHost']
983                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)
984             else:
985                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
986             Line = Sanitize(Line) + "\n"
987             F.write(Line)
988    # Oops, something unspeakable happened.
989    except:
990       Die(File, F, None)
991       raise
992    Done(File, F, None)
993
994 # Generate the debianhosts file (list of all IP addresses)
995 def GenHosts(File):
996    F = None
997    try:
998       OldMask = os.umask(0022)
999       F = open(File + ".tmp", "w", 0644)
1000       os.umask(OldMask)
1001      
1002       seen = set()
1003
1004       global HostAttrs
1005
1006       for x in HostAttrs:
1007
1008          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1009             continue
1010
1011          if not 'ipHostNumber' in x[1]:
1012             continue
1013
1014          addrs = x[1]["ipHostNumber"]
1015          for addr in addrs:
1016             if addr not in seen:
1017                seen.add(addr)
1018                addr = Sanitize(addr) + "\n"
1019                F.write(addr)
1020
1021    # Oops, something unspeakable happened.
1022    except:
1023       Die(File, F, None)
1024       raise
1025    Done(File, F, None)
1026
1027 def GenKeyrings(OutDir):
1028    for k in Keyrings:
1029       shutil.copy(k, OutDir)
1030
1031 # Connect to the ldap server
1032 l = connectLDAP()
1033 # for testing purposes it's sometimes useful to pass username/password
1034 # via the environment
1035 if 'UD_CREDENTIALS' in os.environ:
1036    Pass = os.environ['UD_CREDENTIALS'].split()
1037 else:
1038    F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1039    Pass = F.readline().strip().split(" ")
1040    F.close()
1041 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1042
1043 # Fetch all the groups
1044 GroupIDMap = {}
1045 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1046                   ["gid", "gidNumber", "subGroup"])
1047
1048 # Generate the SubGroupMap and GroupIDMap
1049 for x in Attrs:
1050    if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1051       continue
1052    if x[1].has_key("gidNumber") == 0:
1053       continue
1054    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1055    if x[1].has_key("subGroup") != 0:
1056       SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1057
1058 # Fetch all the users
1059 PasswdAttrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0)))",\
1060                 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1061                  "gecos", "loginShell", "userPassword", "shadowLastChange",\
1062                  "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1063                  "shadowExpire", "emailForward", "latitude", "longitude",\
1064                  "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1065                  "keyFingerPrint", "privateSub", "mailDisableMessage",\
1066                  "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1067                  "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1068                  "mailContentInspectionAction"])
1069
1070 if PasswdAttrs is None:
1071    raise UDEmptyList, "No Users"
1072
1073 PasswdAttrs.sort(lambda x, y: cmp((GetAttr(x, "uid")).lower(), (GetAttr(y, "uid")).lower()))
1074
1075 # Fetch all the hosts
1076 HostAttrs    = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1077                 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1078                  "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1079
1080 if HostAttrs == None:
1081    raise UDEmptyList, "No Hosts"
1082
1083 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1084
1085 # override globaldir for testing
1086 if 'UD_GENERATEDIR' in os.environ:
1087    GenerateDir = os.environ['UD_GENERATEDIR']
1088
1089 # Generate global things
1090 GlobalDir = GenerateDir + "/"
1091 GenDisabledAccounts(GlobalDir + "disabled-accounts")
1092
1093 PasswdAttrs = filter(lambda x: not IsRetired(x), PasswdAttrs)
1094 DebianDDUsers = filter(lambda x: IsGidDebian(x), PasswdAttrs)
1095
1096 CheckForward()
1097
1098 GenMailDisable(GlobalDir + "mail-disable")
1099 GenCDB(GlobalDir + "mail-forward.cdb", PasswdAttrs, 'emailForward')
1100 GenCDB(GlobalDir + "mail-contentinspectionaction.cdb", PasswdAttrs, 'mailContentInspectionAction')
1101 GenPrivate(GlobalDir + "debian-private")
1102 GenSSHKnown(GlobalDir+"authorized_keys", 'authorized_keys')
1103 GenMailBool(GlobalDir + "mail-greylist", "mailGreylisting")
1104 GenMailBool(GlobalDir + "mail-callout", "mailCallout")
1105 GenMailList(GlobalDir + "mail-rbl", "mailRBL")
1106 GenMailList(GlobalDir + "mail-rhsbl", "mailRHSBL")
1107 GenMailList(GlobalDir + "mail-whitelist", "mailWhitelist")
1108 GenKeyrings(GlobalDir)
1109
1110 # Compatibility.
1111 GenForward(GlobalDir + "forward-alias")
1112
1113 PasswdAttrs = filter(lambda x: not x in DisabledUsers, PasswdAttrs)
1114
1115 SSHFiles = GenSSHShadow()
1116 GenMarkers(GlobalDir + "markers")
1117 GenSSHKnown(GlobalDir + "ssh_known_hosts")
1118 GenHosts(GlobalDir + "debianhosts")
1119
1120 for host in HostAttrs:
1121    if not "hostname" in host[1]:
1122       continue
1123
1124    CurrentHost = host[1]['hostname'][0]
1125    OutDir = GenerateDir + '/' + CurrentHost + '/'
1126    try:
1127       os.mkdir(OutDir)
1128    except: 
1129       pass
1130
1131    # Get the group list and convert any named groups to numerics
1132    GroupList = {}
1133    for groupname in AllowedGroupsPreload.strip().split(" "):
1134       GroupList[groupname] = True
1135    if 'allowedGroups' in host[1]:
1136       for groupname in host[1]['allowedGroups']:
1137          GroupList[groupname] = True
1138    for groupname in GroupList.keys():
1139       if groupname in GroupIDMap:
1140          GroupList[str(GroupIDMap[groupname])] = True
1141
1142    ExtraList = {}
1143    if 'exportOptions' in host[1]:
1144       for extra in host[1]['exportOptions']:
1145          ExtraList[extra.upper()] = True
1146
1147    Allowed = GroupList
1148    if Allowed == {}:
1149       Allowed = None
1150
1151    DoLink(GlobalDir, OutDir, "debianhosts")
1152    DoLink(GlobalDir, OutDir, "ssh_known_hosts")
1153    DoLink(GlobalDir, OutDir, "disabled-accounts")
1154
1155    sys.stdout.flush()
1156    if 'NOPASSWD' in ExtraList:
1157       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "*")
1158    else:
1159       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "x")
1160    sys.stdout.flush()
1161    grouprevmap = GenGroup(OutDir + "group")
1162    GenShadowSudo(OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1163
1164    # Now we know who we're allowing on the machine, export
1165    # the relevant ssh keys
1166    GenSSHtarballs(userlist, SSHFiles, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1167
1168    if not 'NOPASSWD' in ExtraList:
1169       GenShadow(OutDir + "shadow")
1170
1171    # Link in global things
1172    if not 'NOMARKERS' in ExtraList:
1173       DoLink(GlobalDir, OutDir, "markers")
1174    DoLink(GlobalDir, OutDir, "mail-forward.cdb")
1175    DoLink(GlobalDir, OutDir, "mail-contentinspectionaction.cdb")
1176    DoLink(GlobalDir, OutDir, "mail-disable")
1177    DoLink(GlobalDir, OutDir, "mail-greylist")
1178    DoLink(GlobalDir, OutDir, "mail-callout")
1179    DoLink(GlobalDir, OutDir, "mail-rbl")
1180    DoLink(GlobalDir, OutDir, "mail-rhsbl")
1181    DoLink(GlobalDir, OutDir, "mail-whitelist")
1182    GenCDB(OutDir + "user-forward.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'emailForward')
1183    GenCDB(OutDir + "batv-tokens.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'bATVToken')
1184    GenCDB(OutDir + "default-mail-options.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'mailDefaultOptions')
1185
1186    # Compatibility.
1187    DoLink(GlobalDir, OutDir, "forward-alias")
1188
1189    if 'DNS' in ExtraList:
1190       GenDNS(OutDir + "dns-zone")
1191       GenZoneRecords(OutDir + "dns-sshfp")
1192
1193    if 'AUTHKEYS' in ExtraList:
1194       DoLink(GlobalDir, OutDir, "authorized_keys")
1195
1196    if 'BSMTP' in ExtraList:
1197       GenBSMTP(OutDir + "bsmtp", HomePrefix)
1198
1199    if 'PRIVATE' in ExtraList:
1200       DoLink(GlobalDir, OutDir, "debian-private")
1201
1202    if 'KEYRING' in ExtraList:
1203       for k in Keyrings:
1204         DoLink(GlobalDir, OutDir, os.path.basename(k))
1205    else:
1206       for k in Keyrings:
1207          try: 
1208             posix.remove(OutDir + os.path.basename(k))
1209          except:
1210             pass
1211
1212 # vim:set et:
1213 # vim:set ts=3:
1214 # vim:set shiftwidth=3: