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