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 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
394 OldMask = os.umask(0077)
395 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
398 if f not in ssh_userkeys:
400 # If we're not exporting their primary group, don't export
403 if userlist[f] in grouprevmap.keys():
404 grname = grouprevmap[userlist[f]]
407 if int(userlist[f]) <= 100:
408 # In these cases, look it up in the normal way so we
409 # deal with cases where, for instance, users are in group
410 # users as their primary group.
411 grname = grp.getgrgid(userlist[f])[0]
416 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])
420 for line in ssh_userkeys[f]:
421 if line.startswith("allowed_hosts=") and ' ' in line:
422 machines, line = line.split('=', 1)[1].split(' ', 1)
423 if current_host not in machines.split(','):
424 continue # skip this key
427 continue # no keys for this host
428 contents = "\n".join(lines) + "\n"
430 to = tarfile.TarInfo(name=f)
431 # These will only be used where the username doesn't
432 # exist on the target system for some reason; hence,
433 # in those cases, the safest thing is for the file to
434 # be owned by root but group nobody. This deals with
435 # the bloody obscure case where the group fails to exist
436 # whilst the user does (in which case we want to avoid
437 # ending up with a file which is owned user:root to avoid
438 # a fairly obvious attack vector)
441 # Using the username / groupname fields avoids any need
442 # to give a shit^W^W^Wcare about the UIDoffset stuff.
446 to.mtime = int(time.time())
447 to.size = len(contents)
449 tf.addfile(to, StringIO(contents))
452 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
454 # add a list of groups to existing groups,
455 # including all subgroups thereof, recursively.
456 # basically this proceduces the transitive hull of the groups in
458 def addGroups(existingGroups, newGroups, uid, current_host):
459 for group in newGroups:
460 # if it's a <group>@host, split it and verify it's on the current host.
461 s = group.split('@', 1)
462 if len(s) == 2 and s[1] != current_host:
466 # let's see if we handled this group already
467 if group in existingGroups:
470 if not GroupIDMap.has_key(group):
471 print "Group", group, "does not exist but", uid, "is in it"
474 existingGroups.append(group)
476 if SubGroupMap.has_key(group):
477 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
479 # Generate the group list
480 def GenGroup(accounts, File, current_host):
484 F = open(File + ".tdb.tmp", "w")
486 # Generate the GroupMap
490 GroupHasPrimaryMembers = {}
492 # Sort them into a list of groups having a set of users
494 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
495 if not 'supplementaryGid' in a: continue
498 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
500 GroupMap[g].append(a['uid'])
502 # Output the group file.
504 for x in GroupMap.keys():
505 if not x in GroupIDMap:
508 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
511 grouprevmap[GroupIDMap[x]] = x
513 Line = "%s:x:%u:" % (x, GroupIDMap[x])
515 for I in GroupMap[x]:
516 Line = Line + ("%s%s" % (Comma, I))
518 Line = Sanitize(Line) + "\n"
519 F.write("0%u %s" % (J, Line))
520 F.write(".%s %s" % (x, Line))
521 F.write("=%u %s" % (GroupIDMap[x], Line))
524 # Oops, something unspeakable happened.
532 def CheckForward(accounts):
534 if not 'emailForward' in a: continue
538 # Do not allow people to try to buffer overflow busted parsers
539 if len(a['emailForward']) > 200: delete = True
540 # Check the forwarding address
541 elif EmailCheck.match(a['emailForward']) is None: delete = True
544 a.delete_mailforward()
546 # Generate the email forwarding list
547 def GenForward(accounts, File):
550 OldMask = os.umask(0022)
551 F = open(File + ".tmp", "w", 0644)
555 if not 'emailForward' in a: continue
556 Line = "%s: %s" % (a['uid'], a['emailForward'])
557 Line = Sanitize(Line) + "\n"
560 # Oops, something unspeakable happened.
566 def GenCDB(accounts, File, key):
569 OldMask = os.umask(0022)
570 # nothing else does the fsync stuff, so why do it here?
571 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
572 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
575 # Write out the email address for each user
577 if not key in a: continue
580 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
583 # Oops, something unspeakable happened.
587 if Fdb.close() != None:
588 raise "cdbmake gave an error"
590 # Generate the anon XEarth marker file
591 def GenMarkers(accounts, File):
594 F = open(File + ".tmp", "w")
596 # Write out the position for each user
598 if not ('latitude' in a and 'longitude' in a): continue
600 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
601 Line = Sanitize(Line) + "\n"
606 # Oops, something unspeakable happened.
612 # Generate the debian-private subscription list
613 def GenPrivate(accounts, File):
616 F = open(File + ".tmp", "w")
618 # Write out the position for each user
620 if not a.is_active_user(): continue
621 if not 'privateSub' in a: continue
623 Line = "%s"%(a['privateSub'])
624 Line = Sanitize(Line) + "\n"
629 # Oops, something unspeakable happened.
635 # Generate a list of locked accounts
636 def GenDisabledAccounts(accounts, File):
639 F = open(File + ".tmp", "w")
640 disabled_accounts = []
642 # Fetch all the users
644 if a.pw_active(): continue
645 Line = "%s:%s" % (a['uid'], "Account is locked")
646 disabled_accounts.append(a)
647 F.write(Sanitize(Line) + "\n")
649 # Oops, something unspeakable happened.
654 return disabled_accounts
656 # Generate the list of local addresses that refuse all mail
657 def GenMailDisable(accounts, File):
660 F = open(File + ".tmp", "w")
663 if not 'mailDisableMessage' in a: continue
664 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
665 Line = Sanitize(Line) + "\n"
668 # Oops, something unspeakable happened.
674 # Generate a list of uids that should have boolean affects applied
675 def GenMailBool(accounts, File, key):
678 F = open(File + ".tmp", "w")
681 if not key in a: continue
682 if not a[key] == 'TRUE': continue
683 Line = "%s"%(a['uid'])
684 Line = Sanitize(Line) + "\n"
687 # Oops, something unspeakable happened.
693 # Generate a list of hosts for RBL or whitelist purposes.
694 def GenMailList(accounts, File, key):
697 F = open(File + ".tmp", "w")
699 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
700 else: validregex = re.compile('^[-\w.]+$')
703 if not key in a: continue
705 filtered = filter(lambda z: validregex.match(z), a[key])
706 if len(filtered) == 0: continue
707 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
708 line = a['uid'] + ': ' + ' : '.join(filtered)
709 line = Sanitize(line) + "\n"
712 # Oops, something unspeakable happened.
718 def isRoleAccount(account):
719 return 'debianRoleAccount' in account['objectClass']
721 # Generate the DNS Zone file
722 def GenDNS(accounts, File):
725 F = open(File + ".tmp", "w")
727 # Fetch all the users
730 # Write out the zone file entry for each user
732 if not 'dnsZoneEntry' in a: continue
733 if not a.is_active_user() and not isRoleAccount(a): continue
736 F.write("; %s\n"%(a.email_address()))
737 for z in a["dnsZoneEntry"]:
738 Split = z.lower().split()
739 if Split[1].lower() == 'in':
740 Line = " ".join(Split) + "\n"
743 Host = Split[0] + DNSZone
744 if BSMTPCheck.match(Line) != None:
745 F.write("; Has BSMTP\n")
747 # Write some identification information
748 if not RRs.has_key(Host):
749 if Split[2].lower() in ["a", "aaaa"]:
750 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
751 for y in a["keyFingerPrint"]:
752 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
756 Line = "; Err %s"%(str(Split))
761 F.write("; Errors:\n")
762 for line in str(e).split("\n"):
763 F.write("; %s\n"%(line))
766 # Oops, something unspeakable happened.
772 def ExtractDNSInfo(x):
776 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
779 if x[1].has_key("ipHostNumber"):
780 for I in x[1]["ipHostNumber"]:
781 if IsV6Addr.match(I) != None:
782 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
784 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
788 if 'sshRSAHostKey' in x[1]:
789 for I in x[1]["sshRSAHostKey"]:
791 if Split[0] == 'ssh-rsa':
793 if Split[0] == 'ssh-dss':
795 if Algorithm == None:
797 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
798 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
800 if 'architecture' in x[1]:
801 Arch = GetAttr(x, "architecture")
803 if x[1].has_key("machine"):
804 Mach = " " + GetAttr(x, "machine")
805 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
807 if x[1].has_key("mXRecord"):
808 for I in x[1]["mXRecord"]:
809 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
813 # Generate the DNS records
814 def GenZoneRecords(host_attrs, File):
817 F = open(File + ".tmp", "w")
819 # Fetch all the hosts
821 if x[1].has_key("hostname") == 0:
824 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
827 DNSInfo = ExtractDNSInfo(x)
831 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
834 Line = "\t\t\t%s" % (Line)
838 # this would write sshfp lines for services on machines
839 # but we can't yet, since some are cnames and we'll make
840 # an invalid zonefile
842 # for i in x[1].get("purpose", []):
843 # m = PurposeHostField.match(i)
846 # # we ignore [[*..]] entries
847 # if m.startswith('*'):
849 # if m.startswith('-'):
852 # if not m.endswith(HostDomain):
854 # if not m.endswith('.'):
856 # for Line in DNSInfo:
857 # if isSSHFP.match(Line):
858 # Line = "%s\t%s" % (m, Line)
859 # F.write(Line + "\n")
861 # Oops, something unspeakable happened.
867 # Generate the BSMTP file
868 def GenBSMTP(accounts, File, HomePrefix):
871 F = open(File + ".tmp", "w")
873 # Write out the zone file entry for each user
875 if not 'dnsZoneEntry' in a: continue
876 if not a.is_active_user(): continue
879 for z in a["dnsZoneEntry"]:
880 Split = z.lower().split()
881 if Split[1].lower() == 'in':
882 for y in range(0, len(Split)):
885 Line = " ".join(Split) + "\n"
887 Host = Split[0] + DNSZone
888 if BSMTPCheck.match(Line) != None:
889 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
890 a['uid'], HomePrefix, a['uid'], Host))
893 F.write("; Errors\n")
896 # Oops, something unspeakable happened.
902 def HostToIP(Host, mapped=True):
906 if Host[1].has_key("ipHostNumber"):
907 for addr in Host[1]["ipHostNumber"]:
908 IPAdresses.append(addr)
909 if IsV6Addr.match(addr) is None and mapped == "True":
910 IPAdresses.append("::ffff:"+addr)
914 # Generate the ssh known hosts file
915 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
918 OldMask = os.umask(0022)
919 F = open(File + ".tmp", "w", 0644)
923 if x[1].has_key("hostname") == 0 or \
924 x[1].has_key("sshRSAHostKey") == 0:
926 Host = GetAttr(x, "hostname")
928 if Host.endswith(HostDomain):
929 HostNames.append(Host[:-(len(HostDomain) + 1)])
931 # in the purpose field [[host|some other text]] (where some other text is optional)
932 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
933 # file. But so that we don't have to add everything we link we can add an asterisk
934 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
935 # http linking it we also support [[-hostname]] entries.
936 for i in x[1].get("purpose", []):
937 m = PurposeHostField.match(i)
940 # we ignore [[*..]] entries
941 if m.startswith('*'):
943 if m.startswith('-'):
947 if m.endswith(HostDomain):
948 HostNames.append(m[:-(len(HostDomain) + 1)])
950 for I in x[1]["sshRSAHostKey"]:
951 if mode and mode == 'authorized_keys':
953 if 'sshdistAuthKeysHost' in x[1]:
954 hosts += x[1]['sshdistAuthKeysHost']
955 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
956 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
957 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
959 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
960 Line = Sanitize(Line) + "\n"
962 # Oops, something unspeakable happened.
968 # Generate the debianhosts file (list of all IP addresses)
969 def GenHosts(host_attrs, File):
972 OldMask = os.umask(0022)
973 F = open(File + ".tmp", "w", 0644)
980 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
983 if not 'ipHostNumber' in x[1]:
986 addrs = x[1]["ipHostNumber"]
990 addr = Sanitize(addr) + "\n"
993 # Oops, something unspeakable happened.
999 def replaceTree(src, dst_basedir):
1000 bn = os.path.basename(src)
1001 dst = os.path.join(dst_basedir, bn)
1003 shutil.copytree(src, dst)
1005 def GenKeyrings(OutDir):
1007 if os.path.isdir(k):
1008 replaceTree(k, OutDir)
1010 shutil.copy(k, OutDir)
1013 def get_accounts(ldap_conn):
1014 # Fetch all the users
1015 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1016 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1017 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1018 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1019 "shadowExpire", "emailForward", "latitude", "longitude",\
1020 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1021 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1022 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1023 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1024 "mailContentInspectionAction", "webPassword"])
1026 if passwd_attrs is None:
1027 raise UDEmptyList, "No Users"
1028 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1029 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1033 def get_hosts(ldap_conn):
1034 # Fetch all the hosts
1035 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1036 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1037 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1039 if HostAttrs == None:
1040 raise UDEmptyList, "No Hosts"
1042 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1047 def make_ldap_conn():
1048 # Connect to the ldap server
1050 # for testing purposes it's sometimes useful to pass username/password
1051 # via the environment
1052 if 'UD_CREDENTIALS' in os.environ:
1053 Pass = os.environ['UD_CREDENTIALS'].split()
1055 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1056 Pass = F.readline().strip().split(" ")
1058 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1064 def setup_group_maps(l):
1065 # Fetch all the groups
1068 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1069 ["gid", "gidNumber", "subGroup"])
1071 # Generate the subgroup_map and group_id_map
1073 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1075 if x[1].has_key("gidNumber") == 0:
1077 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1078 if x[1].has_key("subGroup") != 0:
1079 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1083 SubGroupMap = subgroup_map
1084 GroupIDMap = group_id_map
1086 def generate_all(global_dir, ldap_conn):
1087 accounts = get_accounts(ldap_conn)
1088 host_attrs = get_hosts(ldap_conn)
1091 # Generate global things
1092 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1094 accounts = filter(lambda x: not IsRetired(x), accounts)
1095 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1097 CheckForward(accounts)
1099 GenMailDisable(accounts, global_dir + "mail-disable")
1100 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1101 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1102 GenPrivate(accounts, global_dir + "debian-private")
1103 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1104 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1105 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1106 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1107 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1108 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1109 GenWebPassword(accounts, global_dir + "web-passwords")
1110 GenKeyrings(global_dir)
1113 GenForward(accounts, global_dir + "forward-alias")
1115 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1116 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1118 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1119 GenMarkers(accounts, global_dir + "markers")
1120 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1121 GenHosts(host_attrs, global_dir + "debianhosts")
1122 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1124 GenDNS(accounts, global_dir + "dns-zone")
1125 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1127 setup_group_maps(ldap_conn)
1129 for host in host_attrs:
1130 if not "hostname" in host[1]:
1132 generate_host(host, global_dir, accounts, ssh_userkeys)
1134 def generate_host(host, global_dir, accounts, ssh_userkeys):
1135 current_host = host[1]['hostname'][0]
1136 OutDir = global_dir + current_host + '/'
1137 if not os.path.isdir(OutDir):
1140 # Get the group list and convert any named groups to numerics
1142 for groupname in AllowedGroupsPreload.strip().split(" "):
1143 GroupList[groupname] = True
1144 if 'allowedGroups' in host[1]:
1145 for groupname in host[1]['allowedGroups']:
1146 GroupList[groupname] = True
1147 for groupname in GroupList.keys():
1148 if groupname in GroupIDMap:
1149 GroupList[str(GroupIDMap[groupname])] = True
1152 if 'exportOptions' in host[1]:
1153 for extra in host[1]['exportOptions']:
1154 ExtraList[extra.upper()] = True
1157 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), accounts)
1159 DoLink(global_dir, OutDir, "debianhosts")
1160 DoLink(global_dir, OutDir, "ssh_known_hosts")
1161 DoLink(global_dir, OutDir, "disabled-accounts")
1164 if 'NOPASSWD' in ExtraList:
1165 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1167 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1169 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1170 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1172 # Now we know who we're allowing on the machine, export
1173 # the relevant ssh keys
1174 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1176 if not 'NOPASSWD' in ExtraList:
1177 GenShadow(accounts, OutDir + "shadow")
1179 # Link in global things
1180 if not 'NOMARKERS' in ExtraList:
1181 DoLink(global_dir, OutDir, "markers")
1182 DoLink(global_dir, OutDir, "mail-forward.cdb")
1183 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1184 DoLink(global_dir, OutDir, "mail-disable")
1185 DoLink(global_dir, OutDir, "mail-greylist")
1186 DoLink(global_dir, OutDir, "mail-callout")
1187 DoLink(global_dir, OutDir, "mail-rbl")
1188 DoLink(global_dir, OutDir, "mail-rhsbl")
1189 DoLink(global_dir, OutDir, "mail-whitelist")
1190 DoLink(global_dir, OutDir, "all-accounts.json")
1191 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1192 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1193 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1196 DoLink(global_dir, OutDir, "forward-alias")
1198 if 'DNS' in ExtraList:
1199 DoLink(global_dir, OutDir, "dns-zone")
1200 DoLink(global_dir, OutDir, "dns-sshfp")
1202 if 'AUTHKEYS' in ExtraList:
1203 DoLink(global_dir, OutDir, "authorized_keys")
1205 if 'BSMTP' in ExtraList:
1206 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1208 if 'PRIVATE' in ExtraList:
1209 DoLink(global_dir, OutDir, "debian-private")
1211 if 'GITOLITE' in ExtraList:
1212 DoLink(global_dir, OutDir, "ssh-gitolite")
1214 if 'WEB-PASSWORDS' in ExtraList:
1215 DoLink(global_dir, OutDir, "web-passwords")
1217 if 'KEYRING' in ExtraList:
1219 bn = os.path.basename(k)
1220 if os.path.isdir(k):
1221 src = os.path.join(global_dir, bn)
1222 replaceTree(src, OutDir)
1224 DoLink(global_dir, OutDir, bn)
1228 bn = os.path.basename(k)
1229 target = os.path.join(OutDir, bn)
1230 if os.path.isdir(target):
1233 posix.remove(target)
1236 DoLink(global_dir, OutDir, "last_update.trace")
1239 def getLastLDAPChangeTime(l):
1240 mods = l.search_s('cn=log',
1241 ldap.SCOPE_ONELEVEL,
1242 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1247 # Sort the list by reqEnd
1248 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1249 # Take the last element in the array
1250 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1254 def getLastKeyringChangeTime():
1257 mt = os.path.getmtime(k)
1263 def getLastBuildTime(gdir):
1264 cache_last_ldap_mod = 0
1265 cache_last_unix_mod = 0
1268 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1269 cache_last_mod=fd.read().split()
1271 cache_last_ldap_mod = cache_last_mod[0]
1272 cache_last_unix_mod = int(cache_last_mod[1])
1273 except IndexError, ValueError:
1277 if e.errno == errno.ENOENT:
1282 return (cache_last_ldap_mod, cache_last_unix_mod)
1285 parser = optparse.OptionParser()
1286 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1287 help="Output directory.")
1288 parser.add_option("-f", "--force", dest="force", action="store_true",
1289 help="Force generation, even if not update to LDAP has happened.")
1291 (options, args) = parser.parse_args()
1296 if options.generatedir is not None:
1297 generate_dir = os.environ['UD_GENERATEDIR']
1298 elif 'UD_GENERATEDIR' in os.environ:
1299 generate_dir = os.environ['UD_GENERATEDIR']
1301 generate_dir = GenerateDir
1304 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1305 lock = get_lock( lockf )
1307 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1310 l = make_ldap_conn()
1312 time_started = int(time.time())
1313 ldap_last_mod = getLastLDAPChangeTime(l)
1314 unix_last_mod = getLastKeyringChangeTime()
1315 cache_last_ldap_mod, cache_last_unix_mod = getLastBuildTime(generate_dir)
1317 need_update = (ldap_last_mod > cache_last_ldap_mod) or (unix_last_mod > cache_last_unix_mod)
1319 if not options.force and not need_update:
1320 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1321 fd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1325 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1326 generate_all(generate_dir, l)
1327 tracefd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1331 if __name__ == "__main__":
1332 if 'UD_PROFILE' in os.environ:
1335 cProfile.run('ud_generate()', "udg_prof")
1336 p = pstats.Stats('udg_prof')
1337 ##p.sort_stats('time').print_stats()
1338 p.sort_stats('cumulative').print_stats()
1344 # vim:set shiftwidth=3: