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