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