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