3 # Generates passwd, shadow and group files from the ldap directory.
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,2009,2010,2011 Peter Palfrader <peter@palfrader.org>
10 # Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
11 # Copyright (c) 2008 Mark Hymers <mhy@debian.org>
12 # Copyright (c) 2008 Luk Claes <luk@debian.org>
13 # Copyright (c) 2008 Thomas Viehmann <tv@beamnet.de>
14 # Copyright (c) 2009 Stephen Gran <steve@lobefin.net>
15 # Copyright (c) 2010 Helmut Grohne <helmut@subdivi.de>
17 # This program is free software; you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation; either version 2 of the License, or
20 # (at your option) any later version.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
31 import string, re, time, ldap, optparse, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp, fcntl
32 from userdir_ldap import *
33 from userdir_exceptions import *
36 from cStringIO import StringIO
38 from StringIO import StringIO
40 import simplejson as json
43 if not '__author__' in json.__dict__:
44 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
45 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
48 sys.stderr.write("You should probably not run ud-generate as root.\n")
60 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
62 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
63 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
64 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
65 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
66 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
67 isSSHFP = re.compile("^\s*IN\s+SSHFP")
68 DNSZone = ".debian.net"
69 Keyrings = ConfModule.sync_keyrings.split(":")
70 GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
73 def safe_makedirs(dir):
77 if e.errno == errno.EEXIST:
86 if e.errno == errno.ENOENT:
91 def get_lock(fn, wait=5*60):
94 ends = time.time() + wait
99 fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
103 if time.time() >= ends:
105 sl = min(sl*2, 10, ends - time.time())
111 return Str.translate(string.maketrans("\n\r\t", "$$$"))
113 def DoLink(From, To, File):
115 posix.remove(To + File)
118 posix.link(From + File, To + File)
120 def IsRetired(account):
122 Looks for accountStatus in the LDAP record and tries to
123 match it against one of the known retired statuses
126 status = account['accountStatus']
128 line = status.split()
131 if status == "inactive":
134 elif status == "memorial":
137 elif status == "retiring":
138 # We'll give them a few extra days over what we said
139 age = 6 * 31 * 24 * 60 * 60
141 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
149 #def IsGidDebian(account):
150 # return account['gidNumber'] == 800
152 # See if this user is in the group list
153 def IsInGroup(account, allowed, current_host):
154 # See if the primary group is in the list
155 if str(account['gidNumber']) in allowed: return True
157 # Check the host based ACL
158 if account.is_allowed_by_hostacl(current_host): return True
160 # See if there are supplementary groups
161 if not 'supplementaryGid' in account: return False
164 addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
166 if allowed.has_key(g):
170 def Die(File, F, Fdb):
176 os.remove(File + ".tmp")
180 os.remove(File + ".tdb.tmp")
184 def Done(File, F, Fdb):
187 os.rename(File + ".tmp", File)
190 os.rename(File + ".tdb.tmp", File + ".tdb")
192 # Generate the password list
193 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
196 F = open(File + ".tdb.tmp", "w")
201 # Do not let people try to buffer overflow some busted passwd parser.
202 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
204 userlist[a['uid']] = a['gidNumber']
205 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
211 HomePrefix, a['uid'],
213 line = Sanitize(line) + "\n"
214 F.write("0%u %s" % (i, line))
215 F.write(".%s %s" % (a['uid'], line))
216 F.write("=%d %s" % (a['uidNumber'], line))
219 # Oops, something unspeakable happened.
225 # Return the list of users so we know which keys to export
228 def GenAllUsers(accounts, file):
231 OldMask = os.umask(0022)
232 f = open(file + ".tmp", "w", 0644)
237 all.append( { 'uid': a['uid'],
238 'uidNumber': a['uidNumber'],
239 'active': a.pw_active() and a.shadow_active() } )
242 # Oops, something unspeakable happened.
248 # Generate the shadow list
249 def GenShadow(accounts, File):
252 OldMask = os.umask(0077)
253 F = open(File + ".tdb.tmp", "w", 0600)
258 # If the account is locked, mark it as such in shadow
259 # See Debian Bug #308229 for why we set it to 1 instead of 0
260 if not a.pw_active(): ShadowExpire = '1'
261 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
262 else: ShadowExpire = ''
265 values.append(a['uid'])
266 values.append(a.get_password())
267 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
268 if key in a: values.append(a[key])
269 else: values.append('')
270 values.append(ShadowExpire)
271 line = ':'.join(values)+':'
272 line = Sanitize(line) + "\n"
273 F.write("0%u %s" % (i, line))
274 F.write(".%s %s" % (a['uid'], line))
277 # Oops, something unspeakable happened.
283 # Generate the sudo passwd file
284 def GenShadowSudo(accounts, File, untrusted, current_host):
287 OldMask = os.umask(0077)
288 F = open(File + ".tmp", "w", 0600)
293 if 'sudoPassword' in a:
294 for entry in a['sudoPassword']:
295 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
298 uuid = Match.group(1)
299 status = Match.group(2)
300 hosts = Match.group(3)
301 cryptedpass = Match.group(4)
303 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
305 for_all = hosts == "*"
306 for_this_host = current_host in hosts.split(',')
307 if not (for_all or for_this_host):
309 # ignore * passwords for untrusted hosts, but copy host specific passwords
310 if for_all and untrusted:
313 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
318 Line = "%s:%s" % (a['uid'], Pass)
319 Line = Sanitize(Line) + "\n"
320 F.write("%s" % (Line))
322 # Oops, something unspeakable happened.
328 # Generate the sudo passwd file
329 def GenSSHGitolite(accounts, File):
332 OldMask = os.umask(0022)
333 F = open(File + ".tmp", "w", 0600)
336 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
338 if not 'sshRSAAuthKey' in a: continue
341 prefix = GitoliteSSHRestrictions.replace('@@USER@@', User)
342 for I in a["sshRSAAuthKey"]:
343 if I.startswith('ssh-'):
344 line = "%s %s"%(prefix, I)
346 line = "%s,%s"%(prefix, I)
347 line = Sanitize(line) + "\n"
350 # Oops, something unspeakable happened.
356 # Generate the shadow list
357 def GenSSHShadow(global_dir, accounts):
358 # Fetch all the users
362 if not 'sshRSAAuthKey' in a: continue
365 for I in a['sshRSAAuthKey']:
366 MultipleLine = "%s" % I
367 MultipleLine = Sanitize(MultipleLine)
368 contents.append(MultipleLine)
369 userkeys[a['uid']] = contents
372 # Generate the webPassword list
373 def GenWebPassword(accounts, File):
376 OldMask = os.umask(0077)
377 F = open(File, "w", 0600)
381 if not 'webPassword' in a: continue
382 if not a.pw_active(): continue
384 Pass = str(a['webPassword'])
385 Line = "%s:%s" % (a['uid'], Pass)
386 Line = Sanitize(Line) + "\n"
387 F.write("%s" % (Line))
393 # Generate the voipPassword list
394 def GenVoipPassword(accounts, File):
397 OldMask = os.umask(0077)
398 F = open(File, "w", 0600)
402 if not 'voipPassword' in a: continue
403 if not a.pw_active(): continue
405 Pass = str(a['voipPassword'])
406 Line = "<user id=\"%s\">\n <params>\n <param name=\"password\" value=\"%s\"/>\n <params />\n</user>" % (a['uid'], Pass)
407 Line = Sanitize(Line) + "\n"
408 F.write("%s" % (Line))
414 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
415 OldMask = os.umask(0077)
416 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
419 if f not in ssh_userkeys:
421 # If we're not exporting their primary group, don't export
424 if userlist[f] in grouprevmap.keys():
425 grname = grouprevmap[userlist[f]]
428 if int(userlist[f]) <= 100:
429 # In these cases, look it up in the normal way so we
430 # deal with cases where, for instance, users are in group
431 # users as their primary group.
432 grname = grp.getgrgid(userlist[f])[0]
437 print "User %s is supposed to have their key exported to host %s but their primary group (gid: %d) isn't in LDAP" % (f, current_host, userlist[f])
441 for line in ssh_userkeys[f]:
442 if line.startswith("allowed_hosts=") and ' ' in line:
443 machines, line = line.split('=', 1)[1].split(' ', 1)
444 if current_host not in machines.split(','):
445 continue # skip this key
448 continue # no keys for this host
449 contents = "\n".join(lines) + "\n"
451 to = tarfile.TarInfo(name=f)
452 # These will only be used where the username doesn't
453 # exist on the target system for some reason; hence,
454 # in those cases, the safest thing is for the file to
455 # be owned by root but group nobody. This deals with
456 # the bloody obscure case where the group fails to exist
457 # whilst the user does (in which case we want to avoid
458 # ending up with a file which is owned user:root to avoid
459 # a fairly obvious attack vector)
462 # Using the username / groupname fields avoids any need
463 # to give a shit^W^W^Wcare about the UIDoffset stuff.
467 to.mtime = int(time.time())
468 to.size = len(contents)
470 tf.addfile(to, StringIO(contents))
473 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
475 # add a list of groups to existing groups,
476 # including all subgroups thereof, recursively.
477 # basically this proceduces the transitive hull of the groups in
479 def addGroups(existingGroups, newGroups, uid, current_host):
480 for group in newGroups:
481 # if it's a <group>@host, split it and verify it's on the current host.
482 s = group.split('@', 1)
483 if len(s) == 2 and s[1] != current_host:
487 # let's see if we handled this group already
488 if group in existingGroups:
491 if not GroupIDMap.has_key(group):
492 print "Group", group, "does not exist but", uid, "is in it"
495 existingGroups.append(group)
497 if SubGroupMap.has_key(group):
498 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
500 # Generate the group list
501 def GenGroup(accounts, File, current_host):
505 F = open(File + ".tdb.tmp", "w")
507 # Generate the GroupMap
511 GroupHasPrimaryMembers = {}
513 # Sort them into a list of groups having a set of users
515 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
516 if not 'supplementaryGid' in a: continue
519 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
521 GroupMap[g].append(a['uid'])
523 # Output the group file.
525 for x in GroupMap.keys():
526 if not x in GroupIDMap:
529 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
532 grouprevmap[GroupIDMap[x]] = x
534 Line = "%s:x:%u:" % (x, GroupIDMap[x])
536 for I in GroupMap[x]:
537 Line = Line + ("%s%s" % (Comma, I))
539 Line = Sanitize(Line) + "\n"
540 F.write("0%u %s" % (J, Line))
541 F.write(".%s %s" % (x, Line))
542 F.write("=%u %s" % (GroupIDMap[x], Line))
545 # Oops, something unspeakable happened.
553 def CheckForward(accounts):
555 if not 'emailForward' in a: continue
559 # Do not allow people to try to buffer overflow busted parsers
560 if len(a['emailForward']) > 200: delete = True
561 # Check the forwarding address
562 elif EmailCheck.match(a['emailForward']) is None: delete = True
565 a.delete_mailforward()
567 # Generate the email forwarding list
568 def GenForward(accounts, File):
571 OldMask = os.umask(0022)
572 F = open(File + ".tmp", "w", 0644)
576 if not 'emailForward' in a: continue
577 Line = "%s: %s" % (a['uid'], a['emailForward'])
578 Line = Sanitize(Line) + "\n"
581 # Oops, something unspeakable happened.
587 def GenCDB(accounts, File, key):
590 OldMask = os.umask(0022)
591 # nothing else does the fsync stuff, so why do it here?
592 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
593 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
596 # Write out the email address for each user
598 if not key in a: continue
601 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
604 # Oops, something unspeakable happened.
608 if Fdb.close() != None:
609 raise "cdbmake gave an error"
611 # Generate the anon XEarth marker file
612 def GenMarkers(accounts, File):
615 F = open(File + ".tmp", "w")
617 # Write out the position for each user
619 if not ('latitude' in a and 'longitude' in a): continue
621 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
622 Line = Sanitize(Line) + "\n"
627 # Oops, something unspeakable happened.
633 # Generate the debian-private subscription list
634 def GenPrivate(accounts, File):
637 F = open(File + ".tmp", "w")
639 # Write out the position for each user
641 if not a.is_active_user(): continue
642 if not 'privateSub' in a: continue
644 Line = "%s"%(a['privateSub'])
645 Line = Sanitize(Line) + "\n"
650 # Oops, something unspeakable happened.
656 # Generate a list of locked accounts
657 def GenDisabledAccounts(accounts, File):
660 F = open(File + ".tmp", "w")
661 disabled_accounts = []
663 # Fetch all the users
665 if a.pw_active(): continue
666 Line = "%s:%s" % (a['uid'], "Account is locked")
667 disabled_accounts.append(a)
668 F.write(Sanitize(Line) + "\n")
670 # Oops, something unspeakable happened.
675 return disabled_accounts
677 # Generate the list of local addresses that refuse all mail
678 def GenMailDisable(accounts, File):
681 F = open(File + ".tmp", "w")
684 if not 'mailDisableMessage' in a: continue
685 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
686 Line = Sanitize(Line) + "\n"
689 # Oops, something unspeakable happened.
695 # Generate a list of uids that should have boolean affects applied
696 def GenMailBool(accounts, File, key):
699 F = open(File + ".tmp", "w")
702 if not key in a: continue
703 if not a[key] == 'TRUE': continue
704 Line = "%s"%(a['uid'])
705 Line = Sanitize(Line) + "\n"
708 # Oops, something unspeakable happened.
714 # Generate a list of hosts for RBL or whitelist purposes.
715 def GenMailList(accounts, File, key):
718 F = open(File + ".tmp", "w")
720 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
721 else: validregex = re.compile('^[-\w.]+$')
724 if not key in a: continue
726 filtered = filter(lambda z: validregex.match(z), a[key])
727 if len(filtered) == 0: continue
728 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
729 line = a['uid'] + ': ' + ' : '.join(filtered)
730 line = Sanitize(line) + "\n"
733 # Oops, something unspeakable happened.
739 def isRoleAccount(account):
740 return 'debianRoleAccount' in account['objectClass']
742 # Generate the DNS Zone file
743 def GenDNS(accounts, File):
746 F = open(File + ".tmp", "w")
748 # Fetch all the users
751 # Write out the zone file entry for each user
753 if not 'dnsZoneEntry' in a: continue
754 if not a.is_active_user() and not isRoleAccount(a): continue
757 F.write("; %s\n"%(a.email_address()))
758 for z in a["dnsZoneEntry"]:
759 Split = z.lower().split()
760 if Split[1].lower() == 'in':
761 Line = " ".join(Split) + "\n"
764 Host = Split[0] + DNSZone
765 if BSMTPCheck.match(Line) != None:
766 F.write("; Has BSMTP\n")
768 # Write some identification information
769 if not RRs.has_key(Host):
770 if Split[2].lower() in ["a", "aaaa"]:
771 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
772 for y in a["keyFingerPrint"]:
773 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
777 Line = "; Err %s"%(str(Split))
782 F.write("; Errors:\n")
783 for line in str(e).split("\n"):
784 F.write("; %s\n"%(line))
787 # Oops, something unspeakable happened.
793 def ExtractDNSInfo(x):
797 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
800 if x[1].has_key("ipHostNumber"):
801 for I in x[1]["ipHostNumber"]:
802 if IsV6Addr.match(I) != None:
803 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
805 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
809 if 'sshRSAHostKey' in x[1]:
810 for I in x[1]["sshRSAHostKey"]:
812 if Split[0] == 'ssh-rsa':
814 if Split[0] == 'ssh-dss':
816 if Algorithm == None:
818 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
819 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
821 if 'architecture' in x[1]:
822 Arch = GetAttr(x, "architecture")
824 if x[1].has_key("machine"):
825 Mach = " " + GetAttr(x, "machine")
826 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
828 if x[1].has_key("mXRecord"):
829 for I in x[1]["mXRecord"]:
830 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
834 # Generate the DNS records
835 def GenZoneRecords(host_attrs, File):
838 F = open(File + ".tmp", "w")
840 # Fetch all the hosts
842 if x[1].has_key("hostname") == 0:
845 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
848 DNSInfo = ExtractDNSInfo(x)
852 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
855 Line = "\t\t\t%s" % (Line)
859 # this would write sshfp lines for services on machines
860 # but we can't yet, since some are cnames and we'll make
861 # an invalid zonefile
863 # for i in x[1].get("purpose", []):
864 # m = PurposeHostField.match(i)
867 # # we ignore [[*..]] entries
868 # if m.startswith('*'):
870 # if m.startswith('-'):
873 # if not m.endswith(HostDomain):
875 # if not m.endswith('.'):
877 # for Line in DNSInfo:
878 # if isSSHFP.match(Line):
879 # Line = "%s\t%s" % (m, Line)
880 # F.write(Line + "\n")
882 # Oops, something unspeakable happened.
888 # Generate the BSMTP file
889 def GenBSMTP(accounts, File, HomePrefix):
892 F = open(File + ".tmp", "w")
894 # Write out the zone file entry for each user
896 if not 'dnsZoneEntry' in a: continue
897 if not a.is_active_user(): continue
900 for z in a["dnsZoneEntry"]:
901 Split = z.lower().split()
902 if Split[1].lower() == 'in':
903 for y in range(0, len(Split)):
906 Line = " ".join(Split) + "\n"
908 Host = Split[0] + DNSZone
909 if BSMTPCheck.match(Line) != None:
910 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
911 a['uid'], HomePrefix, a['uid'], Host))
914 F.write("; Errors\n")
917 # Oops, something unspeakable happened.
923 def HostToIP(Host, mapped=True):
927 if Host[1].has_key("ipHostNumber"):
928 for addr in Host[1]["ipHostNumber"]:
929 IPAdresses.append(addr)
930 if IsV6Addr.match(addr) is None and mapped == "True":
931 IPAdresses.append("::ffff:"+addr)
935 # Generate the ssh known hosts file
936 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
939 OldMask = os.umask(0022)
940 F = open(File + ".tmp", "w", 0644)
944 if x[1].has_key("hostname") == 0 or \
945 x[1].has_key("sshRSAHostKey") == 0:
947 Host = GetAttr(x, "hostname")
949 if Host.endswith(HostDomain):
950 HostNames.append(Host[:-(len(HostDomain) + 1)])
952 # in the purpose field [[host|some other text]] (where some other text is optional)
953 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
954 # file. But so that we don't have to add everything we link we can add an asterisk
955 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
956 # http linking it we also support [[-hostname]] entries.
957 for i in x[1].get("purpose", []):
958 m = PurposeHostField.match(i)
961 # we ignore [[*..]] entries
962 if m.startswith('*'):
964 if m.startswith('-'):
968 if m.endswith(HostDomain):
969 HostNames.append(m[:-(len(HostDomain) + 1)])
971 for I in x[1]["sshRSAHostKey"]:
972 if mode and mode == 'authorized_keys':
974 if 'sshdistAuthKeysHost' in x[1]:
975 hosts += x[1]['sshdistAuthKeysHost']
976 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
977 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
978 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
980 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
981 Line = Sanitize(Line) + "\n"
983 # Oops, something unspeakable happened.
989 # Generate the debianhosts file (list of all IP addresses)
990 def GenHosts(host_attrs, File):
993 OldMask = os.umask(0022)
994 F = open(File + ".tmp", "w", 0644)
1001 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1004 if not 'ipHostNumber' in x[1]:
1007 addrs = x[1]["ipHostNumber"]
1009 if addr not in seen:
1011 addr = Sanitize(addr) + "\n"
1014 # Oops, something unspeakable happened.
1020 def replaceTree(src, dst_basedir):
1021 bn = os.path.basename(src)
1022 dst = os.path.join(dst_basedir, bn)
1024 shutil.copytree(src, dst)
1026 def GenKeyrings(OutDir):
1028 if os.path.isdir(k):
1029 replaceTree(k, OutDir)
1031 shutil.copy(k, OutDir)
1034 def get_accounts(ldap_conn):
1035 # Fetch all the users
1036 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1037 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1038 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1039 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1040 "shadowExpire", "emailForward", "latitude", "longitude",\
1041 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1042 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1043 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1044 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1045 "mailContentInspectionAction", "webPassword", "voipPassword"])
1047 if passwd_attrs is None:
1048 raise UDEmptyList, "No Users"
1049 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1050 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1054 def get_hosts(ldap_conn):
1055 # Fetch all the hosts
1056 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1057 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1058 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1060 if HostAttrs == None:
1061 raise UDEmptyList, "No Hosts"
1063 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1068 def make_ldap_conn():
1069 # Connect to the ldap server
1071 # for testing purposes it's sometimes useful to pass username/password
1072 # via the environment
1073 if 'UD_CREDENTIALS' in os.environ:
1074 Pass = os.environ['UD_CREDENTIALS'].split()
1076 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1077 Pass = F.readline().strip().split(" ")
1079 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1085 def setup_group_maps(l):
1086 # Fetch all the groups
1089 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1090 ["gid", "gidNumber", "subGroup"])
1092 # Generate the subgroup_map and group_id_map
1094 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1096 if x[1].has_key("gidNumber") == 0:
1098 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1099 if x[1].has_key("subGroup") != 0:
1100 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1104 SubGroupMap = subgroup_map
1105 GroupIDMap = group_id_map
1107 def generate_all(global_dir, ldap_conn):
1108 accounts = get_accounts(ldap_conn)
1109 host_attrs = get_hosts(ldap_conn)
1112 # Generate global things
1113 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1115 accounts = filter(lambda x: not IsRetired(x), accounts)
1116 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1118 CheckForward(accounts)
1120 GenMailDisable(accounts, global_dir + "mail-disable")
1121 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1122 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1123 GenPrivate(accounts, global_dir + "debian-private")
1124 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1125 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1126 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1127 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1128 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1129 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1130 GenWebPassword(accounts, global_dir + "web-passwords")
1131 GenVoipPassword(accounts, global_dir + "voip-passwords")
1132 GenKeyrings(global_dir)
1135 GenForward(accounts, global_dir + "forward-alias")
1137 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1138 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1140 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1141 GenMarkers(accounts, global_dir + "markers")
1142 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1143 GenHosts(host_attrs, global_dir + "debianhosts")
1144 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1146 GenDNS(accounts, global_dir + "dns-zone")
1147 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1149 setup_group_maps(ldap_conn)
1151 for host in host_attrs:
1152 if not "hostname" in host[1]:
1154 generate_host(host, global_dir, accounts, ssh_userkeys)
1156 def generate_host(host, global_dir, accounts, ssh_userkeys):
1157 current_host = host[1]['hostname'][0]
1158 OutDir = global_dir + current_host + '/'
1159 if not os.path.isdir(OutDir):
1162 # Get the group list and convert any named groups to numerics
1164 for groupname in AllowedGroupsPreload.strip().split(" "):
1165 GroupList[groupname] = True
1166 if 'allowedGroups' in host[1]:
1167 for groupname in host[1]['allowedGroups']:
1168 GroupList[groupname] = True
1169 for groupname in GroupList.keys():
1170 if groupname in GroupIDMap:
1171 GroupList[str(GroupIDMap[groupname])] = True
1174 if 'exportOptions' in host[1]:
1175 for extra in host[1]['exportOptions']:
1176 ExtraList[extra.upper()] = True
1179 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), accounts)
1181 DoLink(global_dir, OutDir, "debianhosts")
1182 DoLink(global_dir, OutDir, "ssh_known_hosts")
1183 DoLink(global_dir, OutDir, "disabled-accounts")
1186 if 'NOPASSWD' in ExtraList:
1187 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1189 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1191 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1192 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1194 # Now we know who we're allowing on the machine, export
1195 # the relevant ssh keys
1196 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1198 if not 'NOPASSWD' in ExtraList:
1199 GenShadow(accounts, OutDir + "shadow")
1201 # Link in global things
1202 if not 'NOMARKERS' in ExtraList:
1203 DoLink(global_dir, OutDir, "markers")
1204 DoLink(global_dir, OutDir, "mail-forward.cdb")
1205 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1206 DoLink(global_dir, OutDir, "mail-disable")
1207 DoLink(global_dir, OutDir, "mail-greylist")
1208 DoLink(global_dir, OutDir, "mail-callout")
1209 DoLink(global_dir, OutDir, "mail-rbl")
1210 DoLink(global_dir, OutDir, "mail-rhsbl")
1211 DoLink(global_dir, OutDir, "mail-whitelist")
1212 DoLink(global_dir, OutDir, "all-accounts.json")
1213 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1214 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1215 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1218 DoLink(global_dir, OutDir, "forward-alias")
1220 if 'DNS' in ExtraList:
1221 DoLink(global_dir, OutDir, "dns-zone")
1222 DoLink(global_dir, OutDir, "dns-sshfp")
1224 if 'AUTHKEYS' in ExtraList:
1225 DoLink(global_dir, OutDir, "authorized_keys")
1227 if 'BSMTP' in ExtraList:
1228 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1230 if 'PRIVATE' in ExtraList:
1231 DoLink(global_dir, OutDir, "debian-private")
1233 if 'GITOLITE' in ExtraList:
1234 DoLink(global_dir, OutDir, "ssh-gitolite")
1236 if 'WEB-PASSWORDS' in ExtraList:
1237 DoLink(global_dir, OutDir, "web-passwords")
1239 if 'KEYRING' in ExtraList:
1241 bn = os.path.basename(k)
1242 if os.path.isdir(k):
1243 src = os.path.join(global_dir, bn)
1244 replaceTree(src, OutDir)
1246 DoLink(global_dir, OutDir, bn)
1250 bn = os.path.basename(k)
1251 target = os.path.join(OutDir, bn)
1252 if os.path.isdir(target):
1255 posix.remove(target)
1258 DoLink(global_dir, OutDir, "last_update.trace")
1261 def getLastLDAPChangeTime(l):
1262 mods = l.search_s('cn=log',
1263 ldap.SCOPE_ONELEVEL,
1264 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1269 # Sort the list by reqEnd
1270 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1271 # Take the last element in the array
1272 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1276 def getLastBuildTime(gdir):
1280 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1281 cache_last_mod=fd.read().split()
1283 cache_last_mod = cache_last_mod[0]
1288 if e.errno == errno.ENOENT:
1293 return cache_last_mod
1297 parser = optparse.OptionParser()
1298 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1299 help="Output directory.")
1300 parser.add_option("-f", "--force", dest="force", action="store_true",
1301 help="Force generation, even if not update to LDAP has happened.")
1303 (options, args) = parser.parse_args()
1308 if options.generatedir is not None:
1309 generate_dir = os.environ['UD_GENERATEDIR']
1310 elif 'UD_GENERATEDIR' in os.environ:
1311 generate_dir = os.environ['UD_GENERATEDIR']
1313 generate_dir = GenerateDir
1316 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1317 lock = get_lock( lockf )
1319 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1322 l = make_ldap_conn()
1324 ldap_last_mod = getLastLDAPChangeTime(l)
1325 cache_last_mod = getLastBuildTime(generate_dir)
1326 need_update = ldap_last_mod > cache_last_mod
1328 if not options.force and not need_update:
1329 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1330 fd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1334 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1335 generate_all(generate_dir, l)
1336 tracefd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1340 if __name__ == "__main__":
1341 if 'UD_PROFILE' in os.environ:
1344 cProfile.run('ud_generate()', "udg_prof")
1345 p = pstats.Stats('udg_prof')
1346 ##p.sort_stats('time').print_stats()
1347 p.sort_stats('cumulative').print_stats()
1353 # vim:set shiftwidth=3: