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