Import from samosa: Added support for locked accounts (*LK* as
[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 import string, re, time, ldap, getopt, sys, os, pwd, posix, socket;
6 from userdir_ldap import *;
7
8 global Allowed;
9 global CurrentHost;
10
11 PasswdAttrs = None;
12 GroupIDMap = {};
13 Allowed = None;
14 CurrentHost = "";
15
16 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$");
17 BSMTPCheck = re.compile(".*mx 0 (klecker|gluck)\.debian\.org\..*",re.DOTALL);
18 DNSZone = ".debian.net"
19
20 def Sanitize(Str):
21   return string.translate(Str,string.maketrans("\n\r\t","$$$"));
22
23 def DoLink(From,To,File):
24    try: posix.remove(To+File);
25    except: pass;
26    posix.link(From+File,To+File);
27
28 # See if this user is in the group list
29 def IsInGroup(DnRecord):
30   if Allowed == None:
31      return 1;
32
33   # See if the primary group is in the list
34   if Allowed.has_key(GetAttr(DnRecord,"gidNumber")) != 0:
35      return 1;
36
37   # Check the host based ACL
38   if DnRecord[1].has_key("allowedHost") != 0:
39      for I in DnRecord[1]["allowedHost"]:
40         if CurrentHost == I:
41            return 1;
42
43   # See if there are supplementary groups
44   if DnRecord[1].has_key("supplementaryGid") == 0:
45      return 0;
46
47   # Check the supplementary groups
48   for I in DnRecord[1]["supplementaryGid"]:
49      if Allowed.has_key(I):
50         return 1;
51   return 0;
52
53 def Die(File,F,Fdb):
54    if F != None:
55       F.close();
56    if Fdb != None:
57       Fdb.close();
58    try: os.remove(File + ".tmp");
59    except: pass;
60    try: os.remove(File + ".tdb.tmp");
61    except: pass;
62
63 def Done(File,F,Fdb):
64   if F != None:
65     F.close();
66     os.rename(File + ".tmp",File);
67   if Fdb != None:
68     Fdb.close();
69     os.rename(File + ".tdb.tmp",File+".tdb");
70   
71 # Generate the password list
72 def GenPasswd(l,File,HomePrefix):
73   F = None;
74   try:
75    F = open(File + ".tdb.tmp","w");
76
77    # Fetch all the users
78    global PasswdAttrs;
79    if PasswdAttrs == None:
80       raise "No Users";
81
82    I = 0;
83    for x in PasswdAttrs:
84       if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
85          continue;
86
87       # Do not let people try to buffer overflow some busted passwd parser.
88       if len(GetAttr(x,"gecos")) > 100 or len(GetAttr(x,"loginShell")) > 50:
89          continue;
90
91       Line = "%s:x:%s:%s:%s:%s%s:%s" % (GetAttr(x,"uid"),\
92               GetAttr(x,"uidNumber"),GetAttr(x,"gidNumber"),\
93               GetAttr(x,"gecos"),HomePrefix,GetAttr(x,"uid"),\
94               GetAttr(x,"loginShell"));
95
96       Line = Sanitize(Line) + "\n";
97       F.write("0%u %s" % (I,Line));
98       F.write(".%s %s" % (GetAttr(x,"uid"),Line));
99       F.write("=%s %s" % (GetAttr(x,"uidNumber"),Line));
100       I = I + 1;
101
102   # Oops, something unspeakable happened.
103   except:
104    Die(File,None,F);
105    raise;
106   Done(File,None,F);
107
108 # Generate the shadow list
109 def GenShadow(l,File):
110   F = None;
111   try:
112    OldMask = os.umask(0077);
113    F = open(File + ".tdb.tmp","w",0600);
114    os.umask(OldMask);
115
116    # Fetch all the users
117    global PasswdAttrs;
118    if PasswdAttrs == None:
119       raise "No Users";
120
121    I = 0;
122    for x in PasswdAttrs:
123       if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
124          continue;
125          
126       Pass = GetAttr(x,"userPassword");
127       if Pass[0:7] != "{crypt}" or len(Pass) > 50:
128          Pass = '*';
129       else:
130          Pass = Pass[7:];
131       Line = "%s:%s:%s:%s:%s:%s:%s:%s:" % (GetAttr(x,"uid"),\
132               Pass,GetAttr(x,"shadowLastChange"),\
133               GetAttr(x,"shadowMin"),GetAttr(x,"shadowMax"),\
134               GetAttr(x,"shadowWarning"),GetAttr(x,"shadowinactive"),\
135               GetAttr(x,"shadowexpire"));
136       Line = Sanitize(Line) + "\n";
137       F.write("0%u %s" % (I,Line));
138       F.write(".%s %s" % (GetAttr(x,"uid"),Line));
139       I = I + 1;
140
141   # Oops, something unspeakable happened.
142   except:
143    Die(File,None,F);
144    raise;
145   Done(File,None,F);
146
147 # Generate the shadow list
148 def GenSSHShadow(l,File):
149   F = None;
150   try:
151    OldMask = os.umask(0077);
152    F = open(File + ".tmp","w",0600);
153    os.umask(OldMask);
154
155    # Fetch all the users
156    global PasswdAttrs;
157    if PasswdAttrs == None:
158       raise "No Users";
159
160    for x in PasswdAttrs:
161       # If the account is locked, do not write it.
162       # This is a partial stop-gap. The ssh also needs to change this
163       # to ignore ~/.ssh/authorized* files.
164       if (string.find(GetAttr(x,"userPassword"),"*LK*")  != -1):
165          continue;
166
167       if x[1].has_key("uidNumber") == 0 or \
168          x[1].has_key("sshRSAAuthKey") == 0:
169          continue;
170       for I in x[1]["sshRSAAuthKey"]:
171          User = GetAttr(x,"uid");
172          Line = "%s: %s" %(User,I);
173          Line = Sanitize(Line) + "\n";
174          F.write(Line);
175   # Oops, something unspeakable happened.
176   except:
177    Die(File,F,None);
178    raise;
179   Done(File,F,None);
180
181 # Generate the group list
182 def GenGroup(l,File):
183   F = None;
184   try:
185    F = open(File + ".tdb.tmp","w");
186
187    # Generate the GroupMap
188    GroupMap = {};
189    for x in GroupIDMap.keys():
190       GroupMap[x] = [];
191       
192    # Fetch all the users
193    global PasswdAttrs;
194    if PasswdAttrs == None:
195       raise "No Users";
196
197    # Sort them into a list of groups having a set of users
198    for x in PasswdAttrs:
199       if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
200          continue;
201       if x[1].has_key("supplementaryGid") == 0:
202          continue;
203          
204       for I in x[1]["supplementaryGid"]:
205          if GroupMap.has_key(I):
206             GroupMap[I].append(GetAttr(x,"uid"));
207          else:
208             print "Group does not exist ",I,"but",GetAttr(x,"uid"),"is in it";
209             
210    # Output the group file.
211    J = 0;
212    for x in GroupMap.keys():
213       if GroupIDMap.has_key(x) == 0:
214          continue;
215       Line = "%s:x:%u:" % (x,GroupIDMap[x]);
216       Comma = '';
217       for I in GroupMap[x]:
218         Line = Line + ("%s%s" % (Comma,I));
219         Comma = ',';
220       Line = Sanitize(Line) + "\n";
221       F.write("0%u %s" % (J,Line));
222       F.write(".%s %s" % (x,Line));
223       F.write("=%u %s" % (GroupIDMap[x],Line));
224       J = J + 1;
225       
226   # Oops, something unspeakable happened.
227   except:
228    Die(File,None,F);
229    raise;
230   Done(File,None,F);
231
232 # Generate the email forwarding list
233 def GenForward(l,File):
234   F = None;
235   Fdb = None;
236   try:
237    OldMask = os.umask(0022);
238    F = open(File + ".tmp","w",0644);
239    os.umask(OldMask);
240
241    # Fetch all the users
242    global PasswdAttrs;
243    if PasswdAttrs == None:
244       raise "No Users";
245
246    # Write out the email address for each user
247    for x in PasswdAttrs:
248       if x[1].has_key("emailForward") == 0 or IsInGroup(x) == 0:
249          continue;
250       
251       # Do not allow people to try to buffer overflow busted parsers
252       if len(GetAttr(x,"emailForward")) > 200:
253          continue;
254
255       # Check the forwarding address
256       if EmailCheck.match(GetAttr(x,"emailForward")) == None:
257          continue;
258       Line = "%s: %s" % (GetAttr(x,"uid"),GetAttr(x,"emailForward"));
259       Line = Sanitize(Line) + "\n";
260       F.write(Line);
261       
262   # Oops, something unspeakable happened.
263   except:
264    Die(File,F,Fdb);
265    raise;
266   Done(File,F,Fdb);
267
268 def GenAllForward(l,File):
269   Fdb = None;
270   try:
271    OldMask = os.umask(0022);
272    Fdb = os.popen("cdbmake %s %s.tmp"%(File,File),"w");
273    os.umask(OldMask);
274
275    # Fetch all the users
276    global PasswdAttrs;
277    if PasswdAttrs == None:
278       raise "No Users";
279
280    # Write out the email address for each user
281    for x in PasswdAttrs:
282       if x[1].has_key("emailForward") == 0:
283          continue;
284       
285       # Do not allow people to try to buffer overflow busted parsers
286       Forward = GetAttr(x,"emailForward");
287       if len(Forward) > 200:
288          continue;
289
290       # Check the forwarding address
291       if EmailCheck.match(Forward) == None:
292          continue;
293          
294       User = GetAttr(x,"uid");
295       Fdb.write("+%d,%d:%s->%s\n"%(len(User),len(Forward),User,Forward));
296    Fdb.write("\n");
297   # Oops, something unspeakable happened.
298   except:
299     Fdb.close();
300     raise;
301   if Fdb.close() != None:
302     raise "cdbmake gave an error";
303
304 # Generate the anon XEarth marker file 
305 def GenMarkers(l,File):
306   F = None;
307   Fdb = None;
308   try:
309    F = open(File + ".tmp","w");
310    Fdb = None;
311
312    # Fetch all the users
313    global PasswdAttrs;
314    if PasswdAttrs == None:
315       raise "No Users";
316
317    # Write out the position for each user
318    for x in PasswdAttrs:
319       if x[1].has_key("latitude") == 0 or x[1].has_key("longitude") == 0:
320          continue;       
321       try:
322          Line = "%8s %8s \"\""%(DecDegree(GetAttr(x,"latitude"),1),DecDegree(GetAttr(x,"longitude"),1));
323          Line = Sanitize(Line) + "\n";
324          F.write(Line);
325       except:
326          pass;
327       
328   # Oops, something unspeakable happened.
329   except:
330    Die(File,F,Fdb);
331    raise;
332   Done(File,F,Fdb);
333
334 # Generate the debian-private subscription list
335 def GenPrivate(l,File):
336   F = None;
337   Fdb = None;
338   try:
339    F = open(File + ".tmp","w");
340    Fdb = None;
341
342    # Fetch all the users
343    global PasswdAttrs;
344    if PasswdAttrs == None:
345       raise "No Users";
346
347    # Write out the position for each user
348    for x in PasswdAttrs:
349       if x[1].has_key("privateSub") == 0:
350          continue;
351
352       # If the account is locked, do not write it
353       if (string.find(GetAttr(x,"userPassword"),"*LK*")  != -1):
354          continue;
355
356       # If the account has no PGP key, do not write it
357       if x[1].has_key("keyFingerPrint") == 0:
358          continue;
359
360       # Must be in the Debian group (yuk, hard coded for now)
361       if GetAttr(x,"gidNumber") != "800":
362          continue;
363
364       try:
365          Line = "%s"%(GetAttr(x,"privateSub"));
366          Line = Sanitize(Line) + "\n";
367          F.write(Line);
368       except:
369          pass;
370       
371   # Oops, something unspeakable happened.
372   except:
373    Die(File,F,Fdb);
374    raise;
375   Done(File,F,Fdb);
376
377 # Generate the DNS Zone file
378 def GenDNS(l,File,HomePrefix):
379   F = None;
380   try:
381    F = open(File + ".tmp","w");
382    
383    # Fetch all the users
384    global PasswdAttrs;
385    if PasswdAttrs == None:
386       raise "No Users";
387
388    # Write out the zone file entry for each user
389    for x in PasswdAttrs:
390       if x[1].has_key("dnsZoneEntry") == 0:
391          continue;
392       try:
393          F.write("; %s\n"%(EmailAddress(x)));
394          for z in x[1]["dnsZoneEntry"]:
395             Split = string.split(string.lower(z));
396             if string.lower(Split[1]) == 'in':
397                for y in range(0,len(Split)):
398                   if Split[y] == "$":
399                      Split[y] = "\n\t";
400                Line = string.join(Split," ") + "\n";
401                F.write(Line);
402                
403                Host = Split[0] + DNSZone;
404                if BSMTPCheck.match(Line) != None:
405                    F.write("; Has BSMTP\n");
406                                
407                # Write some identification information
408                if string.lower(Split[2]) == "a":
409                   Line = "%s IN TXT \"%s\"\n"%(Split[0],EmailAddress(x));
410                   for y in x[1]["keyFingerPrint"]:
411                      Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0],FormatPGPKey(y));
412                   F.write(Line);
413             else:
414                Line = "; Err %s"%(str(Split));
415                F.write(Line);
416
417          F.write("\n");
418       except:
419          F.write("; Errors\n");
420          pass;
421       
422   # Oops, something unspeakable happened.
423   except:
424    Die(File,F,None);
425    raise;
426   Done(File,F,None);
427
428 # Generate the BSMTP file
429 def GenBSMTP(l,File,HomePrefix):
430   F = None;
431   try:
432    F = open(File + ".tmp","w");
433    
434    # Fetch all the users
435    global PasswdAttrs;
436    if PasswdAttrs == None:
437       raise "No Users";
438
439    # Write out the zone file entry for each user
440    for x in PasswdAttrs:
441       if x[1].has_key("dnsZoneEntry") == 0:
442          continue;
443       try:
444          for z in x[1]["dnsZoneEntry"]:
445             Split = string.split(string.lower(z));
446             if string.lower(Split[1]) == 'in':
447                for y in range(0,len(Split)):
448                   if Split[y] == "$":
449                      Split[y] = "\n\t";
450                Line = string.join(Split," ") + "\n";
451                
452                Host = Split[0] + DNSZone;
453                if BSMTPCheck.match(Line) != None:
454                    F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
455                                GetAttr(x,"uid"),HomePrefix,GetAttr(x,"uid"),Host));
456                                
457       except:
458          F.write("; Errors\n");
459          pass;
460       
461   # Oops, something unspeakable happened.
462   except:
463    Die(File,F,None);
464    raise;
465   Done(File,F,None);
466
467 # Generate the shadow list
468 def GenSSHKnown(l,File):
469   F = None;
470   try:
471    OldMask = os.umask(0022);
472    F = open(File + ".tmp","w",0644);
473    os.umask(OldMask);
474
475    # Fetch all the hosts
476    HostKeys = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"sshRSAHostKey=*",\
477                 ["hostname","sshRSAHostKey"]);
478    
479    if HostKeys == None:
480       raise "No Hosts";
481
482    for x in HostKeys:
483       if x[1].has_key("hostname") == 0 or \
484          x[1].has_key("sshRSAHostKey") == 0:
485          continue;
486       Host = GetAttr(x,"hostname");
487       SHost = string.find(Host,".");
488       for I in x[1]["sshRSAHostKey"]:
489          if SHost == None:
490             Line = "%s,%s %s" %(Host,socket.gethostbyname(Host),I);
491          else:
492             Line = "%s,%s,%s %s" %(Host,Host[0:SHost],socket.gethostbyname(Host),I);
493          Line = Sanitize(Line) + "\n";
494          F.write(Line);
495   # Oops, something unspeakable happened.
496   except:
497    Die(File,F,None);
498    raise;
499   Done(File,F,None);
500
501
502 # Connect to the ldap server
503 l = ldap.open(LDAPServer);
504 F = open(PassDir+"/pass-"+pwd.getpwuid(os.getuid())[0],"r");
505 Pass = string.split(string.strip(F.readline())," ");
506 F.close();
507 l.simple_bind_s("uid="+Pass[0]+","+BaseDn,Pass[1]);
508
509 # Fetch all the groups
510 GroupIDMap = {};
511 Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"gid=*",\
512                   ["gid","gidNumber"]);
513
514 # Generate the GroupMap and GroupIDMap
515 for x in Attrs:
516    if x[1].has_key("gidNumber") == 0:
517       continue;
518    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0]);
519
520 # Fetch all the users
521 PasswdAttrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=*",\
522                 ["uid","uidNumber","gidNumber","supplementaryGid",\
523                  "gecos","loginShell","userPassword","shadowLastChange",\
524                  "shadowMin","shadowMax","shadowWarning","shadowinactive",
525                  "shadowexpire","emailForward","latitude","longitude",\
526                  "allowedHost","sshRSAAuthKey","dnsZoneEntry","cn","sn",\
527                  "keyFingerPrint","privateSub"]);
528
529 # Open the control file
530 if len(sys.argv) == 1:
531    F = open(GenerateConf,"r");
532 else:
533    F = open(sys.argv[1],"r")
534
535 # Generate global things
536 GlobalDir = GenerateDir+"/";
537 GenSSHShadow(l,GlobalDir+"ssh-rsa-shadow");
538 GenAllForward(l,GlobalDir+"mail-forward.cdb");
539 GenMarkers(l,GlobalDir+"markers");
540 GenPrivate(l,GlobalDir+"debian-private");
541 GenSSHKnown(l,GlobalDir+"ssh_known_hosts");
542
543 # Compatibility.
544 GenForward(l,GlobalDir+"forward-alias");
545    
546 while(1):
547    Line = F.readline();
548    if Line == "":
549       break;
550    Line = string.strip(Line);
551    if Line == "":
552       continue;
553    if Line[0] == '#':
554       continue;
555
556    Split = string.split(Line," ");
557    OutDir = GenerateDir + '/' + Split[0] + '/';
558    try: os.mkdir(OutDir);
559    except: pass;
560
561    # Get the group list and convert any named groups to numerics
562    GroupList = {};
563    ExtraList = {};
564    for I in Split[2:]:
565       if I[0] == '[':
566          ExtraList[I] = None;
567          continue;
568       GroupList[I] = None;
569       if GroupIDMap.has_key(I):
570          GroupList[str(GroupIDMap[I])] = None;
571
572    Allowed = GroupList;
573    CurrentHost = Split[0];
574
575    sys.stdout.flush();
576    GenPasswd(l,OutDir+"passwd",Split[1]);
577    sys.stdout.flush();
578    GenGroup(l,OutDir+"group");
579    GenShadow(l,OutDir+"shadow");
580         
581    # Link in global things   
582    DoLink(GlobalDir,OutDir,"ssh-rsa-shadow");
583    DoLink(GlobalDir,OutDir,"markers");
584    DoLink(GlobalDir,OutDir,"mail-forward.cdb");
585    DoLink(GlobalDir,OutDir,"ssh_known_hosts");
586
587    # Compatibility.
588    DoLink(GlobalDir,OutDir,"forward-alias");
589
590    if ExtraList.has_key("[DNS]"):
591       GenDNS(l,OutDir+"dns-zone",Split[1]);
592       
593    if ExtraList.has_key("[BSMTP]"):
594       GenBSMTP(l,OutDir+"bsmtp",Split[1]);
595
596    if ExtraList.has_key("[PRIVATE]"):
597       DoLink(GlobalDir,OutDir,"debian-private");
598