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