GenSSHShadow
[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       a = UDLdap.Account(x[0], x[1])
328       if not 'sshRSAAuthKey' in a: continue
329
330       F = None
331       try:
332          OldMask = os.umask(0077)
333          File = os.path.join(GlobalDir, 'userkeys', a['uid'])
334          F = open(File + ".tmp", "w", 0600)
335          os.umask(OldMask)
336
337          for I in a['sshRSAAuthKey']:
338             MultipleLine = "%s" % I
339             MultipleLine = Sanitize(MultipleLine) + "\n"
340             F.write(MultipleLine)
341
342          Done(File, F, None)
343          userfiles.append(os.path.basename(File))
344
345       # Oops, something unspeakable happened.
346       except IOError:
347          Die(File, F, None)
348          # As neither masterFileName nor masterFile are defined at any point
349          # this will raise a NameError.
350          Die(masterFileName, masterFile, None)
351          raise
352
353    return userfiles
354
355 def GenSSHtarballs(userlist, SSHFiles, grouprevmap, target):
356    OldMask = os.umask(0077)
357    tf = tarfile.open(name=os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
358    os.umask(OldMask)
359    for f in userlist.keys():
360       if f not in SSHFiles:
361          continue
362       # If we're not exporting their primary group, don't export
363       # the key and warn
364       grname = None
365       if userlist[f] in grouprevmap.keys():
366          grname = grouprevmap[userlist[f]]
367       else:
368          try:
369             if int(userlist[f]) <= 100:
370                # In these cases, look it up in the normal way so we
371                # deal with cases where, for instance, users are in group
372                # users as their primary group.
373                grname = grp.getgrgid(userlist[f])[0]
374          except Exception, e:
375             pass
376
377       if grname is None:
378          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])
379          continue
380
381       to = tf.gettarinfo(os.path.join(GlobalDir, 'userkeys', f), f)
382       # These will only be used where the username doesn't
383       # exist on the target system for some reason; hence,
384       # in those cases, the safest thing is for the file to
385       # be owned by root but group nobody.  This deals with
386       # the bloody obscure case where the group fails to exist
387       # whilst the user does (in which case we want to avoid
388       # ending up with a file which is owned user:root to avoid
389       # a fairly obvious attack vector)
390       to.uid = 0
391       to.gid = 65534
392       # Using the username / groupname fields avoids any need
393       # to give a shit^W^W^Wcare about the UIDoffset stuff.
394       to.uname = f
395       to.gname = grname
396       to.mode  = 0400
397
398       contents = file(os.path.join(GlobalDir, 'userkeys', f)).read()
399       lines = []
400       for line in contents.splitlines():
401          if line.startswith("allowed_hosts=") and ' ' in line:
402             machines, line = line.split('=', 1)[1].split(' ', 1)
403             if CurrentHost not in machines.split(','):
404                continue # skip this key
405          lines.append(line)
406       if not lines:
407          continue # no keys for this host
408       contents = "\n".join(lines) + "\n"
409       to.size = len(contents)
410       tf.addfile(to, StringIO(contents))
411
412    tf.close()
413    os.rename(os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
414
415 # add a list of groups to existing groups,
416 # including all subgroups thereof, recursively.
417 # basically this proceduces the transitive hull of the groups in
418 # addgroups.
419 def addGroups(existingGroups, newGroups, uid):
420    for group in newGroups:
421       # if it's a <group>@host, split it and verify it's on the current host.
422       s = group.split('@', 1)
423       if len(s) == 2 and s[1] != CurrentHost:
424          continue
425       group = s[0]
426
427       # let's see if we handled this group already
428       if group in existingGroups:
429          continue
430
431       if not GroupIDMap.has_key(group):
432          print "Group", group, "does not exist but", uid, "is in it"
433          continue
434
435       existingGroups.append(group)
436
437       if SubGroupMap.has_key(group):
438          addGroups(existingGroups, SubGroupMap[group], uid)
439
440 # Generate the group list
441 def GenGroup(File):
442    grouprevmap = {}
443    F = None
444    try:
445       F = open(File + ".tdb.tmp", "w")
446      
447       # Generate the GroupMap
448       GroupMap = {}
449       for x in GroupIDMap.keys():
450          GroupMap[x] = []
451       GroupHasPrimaryMembers = {}
452      
453       # Fetch all the users
454       global PasswdAttrs
455      
456       # Sort them into a list of groups having a set of users
457       for x in PasswdAttrs:
458          a = UDLdap.Account(x[0], x[1])
459          GroupHasPrimaryMembers[ a['gidNumber'] ] = True
460          if not IsInGroup(x): continue
461          if not 'supplementaryGid' in a: continue
462
463          supgroups=[]
464          addGroups(supgroups, a['supplementaryGid'], a['uid'])
465          for g in supgroups:
466             GroupMap[g].append(a['uid'])
467
468       # Output the group file.
469       J = 0
470       for x in GroupMap.keys():
471          if GroupIDMap.has_key(x) == 0:
472             continue
473
474          if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
475             continue
476
477          grouprevmap[GroupIDMap[x]] = x
478
479          Line = "%s:x:%u:" % (x, GroupIDMap[x])
480          Comma = ''
481          for I in GroupMap[x]:
482             Line = Line + ("%s%s" % (Comma, I))
483             Comma = ','
484          Line = Sanitize(Line) + "\n"
485          F.write("0%u %s" % (J, Line))
486          F.write(".%s %s" % (x, Line))
487          F.write("=%u %s" % (GroupIDMap[x], Line))
488          J = J + 1
489   
490    # Oops, something unspeakable happened.
491    except:
492       Die(File, None, F)
493       raise
494    Done(File, None, F)
495   
496    return grouprevmap
497
498 def CheckForward():
499    global PasswdAttrs
500    for x in PasswdAttrs:
501       if x[1].has_key("emailForward") == 0:
502          continue
503    
504       if not IsInGroup(x):
505          x[1].pop("emailForward")
506          continue
507
508       # Do not allow people to try to buffer overflow busted parsers
509       if len(GetAttr(x, "emailForward")) > 200:
510          x[1].pop("emailForward")
511          continue
512
513       # Check the forwarding address
514       if EmailCheck.match(GetAttr(x, "emailForward")) == None:
515          x[1].pop("emailForward")
516
517 # Generate the email forwarding list
518 def GenForward(File):
519    F = None
520    try:
521       OldMask = os.umask(0022)
522       F = open(File + ".tmp", "w", 0644)
523       os.umask(OldMask)
524      
525       # Fetch all the users
526       global PasswdAttrs
527      
528       # Write out the email address for each user
529       for x in PasswdAttrs:
530          a = UDLdap.Account(x[0], x[1])
531          if not 'emailForward' in a: continue
532          Line = "%s: %s" % (a['uid'], a['emailForward'])
533          Line = Sanitize(Line) + "\n"
534          F.write(Line)
535   
536    # Oops, something unspeakable happened.
537    except:
538       Die(File, F, None)
539       raise
540    Done(File, F, None)
541
542 def GenCDB(File, Users, key):
543    Fdb = None
544    try:
545       OldMask = os.umask(0022)
546       Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
547       os.umask(OldMask)
548
549       # Write out the email address for each user
550       for x in Users:
551          a = UDLdap.Account(x[0], x[1])
552          if not key in a: continue
553          value = a[key]
554          user = a['uid']
555          Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
556
557       Fdb.write("\n")
558    # Oops, something unspeakable happened.
559    except:
560       Fdb.close()
561       raise
562    if Fdb.close() != None:
563       raise "cdbmake gave an error"
564
565 # Generate the anon XEarth marker file
566 def GenMarkers(File):
567    F = None
568    try:
569       F = open(File + ".tmp", "w")
570      
571       # Fetch all the users
572       global PasswdAttrs
573      
574       # Write out the position for each user
575       for x in PasswdAttrs:
576          a = UDLdap.Account(x[0], x[1])
577          if not ('latitude' in a and 'longitude' in a): continue
578          try:
579             Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
580             Line = Sanitize(Line) + "\n"
581             F.write(Line)
582          except:
583             pass
584   
585    # Oops, something unspeakable happened.
586    except:
587       Die(File, F, None)
588       raise
589    Done(File, F, None)
590
591 # Generate the debian-private subscription list
592 def GenPrivate(File):
593    F = None
594    try:
595       F = open(File + ".tmp", "w")
596      
597       # Fetch all the users
598       global DebianDDUsers
599      
600       # Write out the position for each user
601       for x in DebianDDUsers:
602          a = UDLdap.Account(x[0], x[1])
603          if not a.is_active_user(): continue
604          if not 'privateSub' in a: continue
605          try:
606             Line = "%s"%(a['privateSub'])
607             Line = Sanitize(Line) + "\n"
608             F.write(Line)
609          except:
610             pass
611   
612    # Oops, something unspeakable happened.
613    except:
614       Die(File, F, None)
615       raise
616    Done(File, F, None)
617
618 # Generate a list of locked accounts
619 def GenDisabledAccounts(File):
620    F = None
621    try:
622       F = open(File + ".tmp", "w")
623      
624       # Fetch all the users
625       global PasswdAttrs
626       global DisabledUsers
627      
628       I = 0
629       for x in PasswdAttrs:
630          a = UDLdap.Account(x[0], x[1])
631          if a.pw_active(): continue
632          Line = "%s:%s" % (a['uid'], "Account is locked")
633          DisabledUsers.append(x)
634          F.write(Sanitize(Line) + "\n")
635
636    # Oops, something unspeakable happened.
637    except:
638       Die(File, F, None)
639       raise
640    Done(File, F, None)
641
642 # Generate the list of local addresses that refuse all mail
643 def GenMailDisable(File):
644    F = None
645    try:
646       F = open(File + ".tmp", "w")
647      
648       # Fetch all the users
649       global PasswdAttrs
650      
651       for x in PasswdAttrs:
652          a = UDLdap.Account(x[0], x[1])
653          if not 'mailDisableMessage' in a: continue
654          Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
655          Line = Sanitize(Line) + "\n"
656          F.write(Line)
657   
658    # Oops, something unspeakable happened.
659    except:
660       Die(File, F, None)
661       raise
662    Done(File, F, None)
663
664 # Generate a list of uids that should have boolean affects applied
665 def GenMailBool(File, key):
666    F = None
667    try:
668       F = open(File + ".tmp", "w")
669      
670       # Fetch all the users
671       global PasswdAttrs
672      
673       for x in PasswdAttrs:
674          a = UDLdap.Account(x[0], x[1])
675          if not key in a: continue
676          if not a[key] == 'TRUE': continue
677          Line = "%s"%(a['uid'])
678          Line = Sanitize(Line) + "\n"
679          F.write(Line)
680
681    # Oops, something unspeakable happened.
682    except:
683       Die(File, F, None)
684       raise
685    Done(File, F, None)
686
687 # Generate a list of hosts for RBL or whitelist purposes.
688 def GenMailList(File, key):
689    F = None
690    try:
691       F = open(File + ".tmp", "w")
692      
693       # Fetch all the users
694       global PasswdAttrs
695      
696       if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
697       else:                      validregex = re.compile('^[-\w.]+$')
698
699       for x in PasswdAttrs:
700          a = UDLdap.Account(x[0], x[1])
701          if not key in a: continue
702
703          filtered = filter(lambda z: validregex.match(z), a[key])
704          if len(filtered) == 0: continue
705          if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
706          line = a['uid'] + ': ' + ' : '.join(filtered)
707          line = Sanitize(line) + "\n"
708          F.write(line)
709
710    # Oops, something unspeakable happened.
711    except:
712       Die(File, F, None)
713       raise
714    Done(File, F, None)
715
716 def isRoleAccount(pwEntry):
717    if not pwEntry.has_key("objectClass"):
718       raise "pwEntry has no objectClass"
719    oc =  pwEntry['objectClass']
720    try:
721       i = oc.index('debianRoleAccount')
722       return True
723    except ValueError:
724       return False
725
726 # Generate the DNS Zone file
727 def GenDNS(File):
728    F = None
729    try:
730       F = open(File + ".tmp", "w")
731      
732       # Fetch all the users
733       global PasswdAttrs
734       RRs = {}
735      
736       # Write out the zone file entry for each user
737       for x in PasswdAttrs:
738          if x[1].has_key("dnsZoneEntry") == 0:
739             continue
740      
741          # If the account has no PGP key, do not write it
742          if x[1].has_key("keyFingerPrint") == 0 and not isRoleAccount(x[1]):
743             continue
744          try:
745             F.write("; %s\n"%(EmailAddress(x)))
746             for z in x[1]["dnsZoneEntry"]:
747                Split = z.lower().split()
748                if Split[1].lower() == 'in':
749                   for y in range(0, len(Split)):
750                      if Split[y] == "$":
751                         Split[y] = "\n\t"
752                   Line = " ".join(Split) + "\n"
753                   F.write(Line)
754      
755                   Host = Split[0] + DNSZone
756                   if BSMTPCheck.match(Line) != None:
757                      F.write("; Has BSMTP\n")
758      
759                   # Write some identification information
760                   if not RRs.has_key(Host):
761                      if Split[2].lower() in ["a", "aaaa"]:
762                         Line = "%s IN TXT \"%s\"\n"%(Split[0], EmailAddress(x))
763                         for y in x[1]["keyFingerPrint"]:
764                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
765                            F.write(Line)
766                         RRs[Host] = 1
767                else:
768                   Line = "; Err %s"%(str(Split))
769                   F.write(Line)
770      
771             F.write("\n")
772          except:
773             F.write("; Errors\n")
774             pass
775   
776    # Oops, something unspeakable happened.
777    except:
778       Die(File, F, None)
779       raise
780    Done(File, F, None)
781
782 def ExtractDNSInfo(x):
783
784    TTLprefix="\t"
785    if 'dnsTTL' in x[1]:
786       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
787
788    DNSInfo = []
789    if x[1].has_key("ipHostNumber"):
790       for I in x[1]["ipHostNumber"]:
791          if IsV6Addr.match(I) != None:
792             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
793          else:
794             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
795
796    Algorithm = None
797
798    if 'sshRSAHostKey' in x[1]:
799       for I in x[1]["sshRSAHostKey"]:
800          Split = I.split()
801          if Split[0] == 'ssh-rsa':
802             Algorithm = 1
803          if Split[0] == 'ssh-dss':
804             Algorithm = 2
805          if Algorithm == None:
806             continue
807          Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
808          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
809
810    if 'architecture' in x[1]:
811       Arch = GetAttr(x, "architecture")
812       Mach = ""
813       if x[1].has_key("machine"):
814          Mach = " " + GetAttr(x, "machine")
815       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
816
817    if x[1].has_key("mXRecord"):
818       for I in x[1]["mXRecord"]:
819          DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
820
821    return DNSInfo
822
823 # Generate the DNS records
824 def GenZoneRecords(File):
825    F = None
826    try:
827       F = open(File + ".tmp", "w")
828
829       # Fetch all the hosts
830       global HostAttrs
831
832       for x in HostAttrs:
833          if x[1].has_key("hostname") == 0:
834             continue
835
836          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
837             continue
838
839          DNSInfo = ExtractDNSInfo(x)
840          start = True
841          for Line in DNSInfo:
842             if start == True:
843                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
844                start = False
845             else:
846                Line = "\t\t\t%s" % (Line)
847
848             F.write(Line + "\n")
849
850         # this would write sshfp lines for services on machines
851         # but we can't yet, since some are cnames and we'll make
852         # an invalid zonefile
853         #
854         # for i in x[1].get("purpose", []):
855         #    m = PurposeHostField.match(i)
856         #    if m:
857         #       m = m.group(1)
858         #       # we ignore [[*..]] entries
859         #       if m.startswith('*'):
860         #          continue
861         #       if m.startswith('-'):
862         #          m = m[1:]
863         #       if m:
864         #          if not m.endswith(HostDomain):
865         #             continue
866         #          if not m.endswith('.'):
867         #             m = m + "."
868         #          for Line in DNSInfo:
869         #             if isSSHFP.match(Line):
870         #                Line = "%s\t%s" % (m, Line)
871         #                F.write(Line + "\n")
872
873    # Oops, something unspeakable happened.
874    except:
875       Die(File, F, None)
876       raise
877    Done(File, F, None)
878
879 # Generate the BSMTP file
880 def GenBSMTP(File, HomePrefix):
881    F = None
882    try:
883       F = open(File + ".tmp", "w")
884      
885       # Fetch all the users
886       global PasswdAttrs
887      
888       # Write out the zone file entry for each user
889       for x in PasswdAttrs:
890          if x[1].has_key("dnsZoneEntry") == 0:
891             continue
892      
893          # If the account has no PGP key, do not write it
894          if x[1].has_key("keyFingerPrint") == 0:
895             continue
896          try:
897             for z in x[1]["dnsZoneEntry"]:
898                Split = z.lower().split()
899                if Split[1].lower() == 'in':
900                   for y in range(0, len(Split)):
901                      if Split[y] == "$":
902                         Split[y] = "\n\t"
903                   Line = " ".join(Split) + "\n"
904      
905                   Host = Split[0] + DNSZone
906                   if BSMTPCheck.match(Line) != None:
907                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
908                                   GetAttr(x, "uid"), HomePrefix, GetAttr(x, "uid"), Host))
909      
910          except:
911             F.write("; Errors\n")
912             pass
913   
914    # Oops, something unspeakable happened.
915    except:
916       Die(File, F, None)
917       raise
918    Done(File, F, None)
919   
920 def HostToIP(Host, mapped=True):
921
922    IPAdresses = []
923
924    if Host[1].has_key("ipHostNumber"):
925       for addr in Host[1]["ipHostNumber"]:
926          IPAdresses.append(addr)
927          if IsV6Addr.match(addr) is None and mapped == "True":
928             IPAdresses.append("::ffff:"+addr)
929
930    return IPAdresses
931
932 # Generate the ssh known hosts file
933 def GenSSHKnown(File, mode=None):
934    F = None
935    try:
936       OldMask = os.umask(0022)
937       F = open(File + ".tmp", "w", 0644)
938       os.umask(OldMask)
939      
940       global HostAttrs
941      
942       for x in HostAttrs:
943          if x[1].has_key("hostname") == 0 or \
944             x[1].has_key("sshRSAHostKey") == 0:
945             continue
946          Host = GetAttr(x, "hostname")
947          HostNames = [ Host ]
948          if Host.endswith(HostDomain):
949             HostNames.append(Host[:-(len(HostDomain) + 1)])
950      
951          # in the purpose field [[host|some other text]] (where some other text is optional)
952          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
953          # file.  But so that we don't have to add everything we link we can add an asterisk
954          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
955          # http linking it we also support [[-hostname]] entries.
956          for i in x[1].get("purpose", []):
957             m = PurposeHostField.match(i)
958             if m:
959                m = m.group(1)
960                # we ignore [[*..]] entries
961                if m.startswith('*'):
962                   continue
963                if m.startswith('-'):
964                   m = m[1:]
965                if m:
966                   HostNames.append(m)
967                   if m.endswith(HostDomain):
968                      HostNames.append(m[:-(len(HostDomain) + 1)])
969      
970          for I in x[1]["sshRSAHostKey"]:
971             if mode and mode == 'authorized_keys':
972                hosts = HostToIP(x)
973                if 'sshdistAuthKeysHost' in x[1]:
974                   hosts += x[1]['sshdistAuthKeysHost']
975                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)
976             else:
977                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
978             Line = Sanitize(Line) + "\n"
979             F.write(Line)
980    # Oops, something unspeakable happened.
981    except:
982       Die(File, F, None)
983       raise
984    Done(File, F, None)
985
986 # Generate the debianhosts file (list of all IP addresses)
987 def GenHosts(File):
988    F = None
989    try:
990       OldMask = os.umask(0022)
991       F = open(File + ".tmp", "w", 0644)
992       os.umask(OldMask)
993      
994       seen = set()
995
996       global HostAttrs
997
998       for x in HostAttrs:
999
1000          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1001             continue
1002
1003          if not 'ipHostNumber' in x[1]:
1004             continue
1005
1006          addrs = x[1]["ipHostNumber"]
1007          for addr in addrs:
1008             if addr not in seen:
1009                seen.add(addr)
1010                addr = Sanitize(addr) + "\n"
1011                F.write(addr)
1012
1013    # Oops, something unspeakable happened.
1014    except:
1015       Die(File, F, None)
1016       raise
1017    Done(File, F, None)
1018
1019 def GenKeyrings(OutDir):
1020    for k in Keyrings:
1021       shutil.copy(k, OutDir)
1022
1023 # Connect to the ldap server
1024 l = connectLDAP()
1025 # for testing purposes it's sometimes useful to pass username/password
1026 # via the environment
1027 if 'UD_CREDENTIALS' in os.environ:
1028    Pass = os.environ['UD_CREDENTIALS'].split()
1029 else:
1030    F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1031    Pass = F.readline().strip().split(" ")
1032    F.close()
1033 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1034
1035 # Fetch all the groups
1036 GroupIDMap = {}
1037 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1038                   ["gid", "gidNumber", "subGroup"])
1039
1040 # Generate the SubGroupMap and GroupIDMap
1041 for x in Attrs:
1042    if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1043       continue
1044    if x[1].has_key("gidNumber") == 0:
1045       continue
1046    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1047    if x[1].has_key("subGroup") != 0:
1048       SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1049
1050 # Fetch all the users
1051 PasswdAttrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0)))",\
1052                 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1053                  "gecos", "loginShell", "userPassword", "shadowLastChange",\
1054                  "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1055                  "shadowExpire", "emailForward", "latitude", "longitude",\
1056                  "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1057                  "keyFingerPrint", "privateSub", "mailDisableMessage",\
1058                  "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1059                  "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1060                  "mailContentInspectionAction"])
1061
1062 if PasswdAttrs is None:
1063    raise UDEmptyList, "No Users"
1064
1065 PasswdAttrs.sort(lambda x, y: cmp((GetAttr(x, "uid")).lower(), (GetAttr(y, "uid")).lower()))
1066
1067 # Fetch all the hosts
1068 HostAttrs    = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1069                 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1070                  "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1071
1072 if HostAttrs == None:
1073    raise UDEmptyList, "No Hosts"
1074
1075 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1076
1077 # override globaldir for testing
1078 if 'UD_GENERATEDIR' in os.environ:
1079    GenerateDir = os.environ['UD_GENERATEDIR']
1080
1081 # Generate global things
1082 GlobalDir = GenerateDir + "/"
1083 GenDisabledAccounts(GlobalDir + "disabled-accounts")
1084
1085 PasswdAttrs = filter(lambda x: not IsRetired(x), PasswdAttrs)
1086 DebianDDUsers = filter(lambda x: IsGidDebian(x), PasswdAttrs)
1087
1088 CheckForward()
1089
1090 GenMailDisable(GlobalDir + "mail-disable")
1091 GenCDB(GlobalDir + "mail-forward.cdb", PasswdAttrs, 'emailForward')
1092 GenCDB(GlobalDir + "mail-contentinspectionaction.cdb", PasswdAttrs, 'mailContentInspectionAction')
1093 GenPrivate(GlobalDir + "debian-private")
1094 GenSSHKnown(GlobalDir+"authorized_keys", 'authorized_keys')
1095 GenMailBool(GlobalDir + "mail-greylist", "mailGreylisting")
1096 GenMailBool(GlobalDir + "mail-callout", "mailCallout")
1097 GenMailList(GlobalDir + "mail-rbl", "mailRBL")
1098 GenMailList(GlobalDir + "mail-rhsbl", "mailRHSBL")
1099 GenMailList(GlobalDir + "mail-whitelist", "mailWhitelist")
1100 GenKeyrings(GlobalDir)
1101
1102 # Compatibility.
1103 GenForward(GlobalDir + "forward-alias")
1104
1105 PasswdAttrs = filter(lambda x: not x in DisabledUsers, PasswdAttrs)
1106
1107 SSHFiles = GenSSHShadow()
1108 GenMarkers(GlobalDir + "markers")
1109 GenSSHKnown(GlobalDir + "ssh_known_hosts")
1110 GenHosts(GlobalDir + "debianhosts")
1111
1112 for host in HostAttrs:
1113    if not "hostname" in host[1]:
1114       continue
1115
1116    CurrentHost = host[1]['hostname'][0]
1117    OutDir = GenerateDir + '/' + CurrentHost + '/'
1118    try:
1119       os.mkdir(OutDir)
1120    except: 
1121       pass
1122
1123    # Get the group list and convert any named groups to numerics
1124    GroupList = {}
1125    for groupname in AllowedGroupsPreload.strip().split(" "):
1126       GroupList[groupname] = True
1127    if 'allowedGroups' in host[1]:
1128       for groupname in host[1]['allowedGroups']:
1129          GroupList[groupname] = True
1130    for groupname in GroupList.keys():
1131       if groupname in GroupIDMap:
1132          GroupList[str(GroupIDMap[groupname])] = True
1133
1134    ExtraList = {}
1135    if 'exportOptions' in host[1]:
1136       for extra in host[1]['exportOptions']:
1137          ExtraList[extra.upper()] = True
1138
1139    Allowed = GroupList
1140    if Allowed == {}:
1141       Allowed = None
1142
1143    DoLink(GlobalDir, OutDir, "debianhosts")
1144    DoLink(GlobalDir, OutDir, "ssh_known_hosts")
1145    DoLink(GlobalDir, OutDir, "disabled-accounts")
1146
1147    sys.stdout.flush()
1148    if 'NOPASSWD' in ExtraList:
1149       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "*")
1150    else:
1151       userlist = GenPasswd(OutDir + "passwd", HomePrefix, "x")
1152    sys.stdout.flush()
1153    grouprevmap = GenGroup(OutDir + "group")
1154    GenShadowSudo(OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1155
1156    # Now we know who we're allowing on the machine, export
1157    # the relevant ssh keys
1158    GenSSHtarballs(userlist, SSHFiles, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1159
1160    if not 'NOPASSWD' in ExtraList:
1161       GenShadow(OutDir + "shadow")
1162
1163    # Link in global things
1164    if not 'NOMARKERS' in ExtraList:
1165       DoLink(GlobalDir, OutDir, "markers")
1166    DoLink(GlobalDir, OutDir, "mail-forward.cdb")
1167    DoLink(GlobalDir, OutDir, "mail-contentinspectionaction.cdb")
1168    DoLink(GlobalDir, OutDir, "mail-disable")
1169    DoLink(GlobalDir, OutDir, "mail-greylist")
1170    DoLink(GlobalDir, OutDir, "mail-callout")
1171    DoLink(GlobalDir, OutDir, "mail-rbl")
1172    DoLink(GlobalDir, OutDir, "mail-rhsbl")
1173    DoLink(GlobalDir, OutDir, "mail-whitelist")
1174    GenCDB(OutDir + "user-forward.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'emailForward')
1175    GenCDB(OutDir + "batv-tokens.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'bATVToken')
1176    GenCDB(OutDir + "default-mail-options.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'mailDefaultOptions')
1177
1178    # Compatibility.
1179    DoLink(GlobalDir, OutDir, "forward-alias")
1180
1181    if 'DNS' in ExtraList:
1182       GenDNS(OutDir + "dns-zone")
1183       GenZoneRecords(OutDir + "dns-sshfp")
1184
1185    if 'AUTHKEYS' in ExtraList:
1186       DoLink(GlobalDir, OutDir, "authorized_keys")
1187
1188    if 'BSMTP' in ExtraList:
1189       GenBSMTP(OutDir + "bsmtp", HomePrefix)
1190
1191    if 'PRIVATE' in ExtraList:
1192       DoLink(GlobalDir, OutDir, "debian-private")
1193
1194    if 'KEYRING' in ExtraList:
1195       for k in Keyrings:
1196         DoLink(GlobalDir, OutDir, os.path.basename(k))
1197    else:
1198       for k in Keyrings:
1199          try: 
1200             posix.remove(OutDir + os.path.basename(k))
1201          except:
1202             pass
1203
1204 # vim:set et:
1205 # vim:set ts=3:
1206 # vim:set shiftwidth=3: