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 *
35 from xml.etree.ElementTree import Element, SubElement, Comment
36 from xml.etree import ElementTree
37 from xml.dom import minidom
39 from cStringIO import StringIO
41 from StringIO import StringIO
43 import simplejson as json
46 if not '__author__' in json.__dict__:
47 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
48 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
51 sys.stderr.write("You should probably not run ud-generate as root.\n")
63 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
65 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
66 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
67 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
68 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
69 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
70 isSSHFP = re.compile("^\s*IN\s+SSHFP")
71 DNSZone = ".debian.net"
72 Keyrings = ConfModule.sync_keyrings.split(":")
73 GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
76 """Return a pretty-printed XML string for the Element.
78 rough_string = ElementTree.tostring(elem, 'utf-8')
79 reparsed = minidom.parseString(rough_string)
80 return reparsed.toprettyxml(indent=" ")
82 def safe_makedirs(dir):
86 if e.errno == errno.EEXIST:
95 if e.errno == errno.ENOENT:
100 def get_lock(fn, wait=5*60):
103 ends = time.time() + wait
108 fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
112 if time.time() >= ends:
114 sl = min(sl*2, 10, ends - time.time())
120 return Str.translate(string.maketrans("\n\r\t", "$$$"))
122 def DoLink(From, To, File):
124 posix.remove(To + File)
127 posix.link(From + File, To + File)
129 def IsRetired(account):
131 Looks for accountStatus in the LDAP record and tries to
132 match it against one of the known retired statuses
135 status = account['accountStatus']
137 line = status.split()
140 if status == "inactive":
143 elif status == "memorial":
146 elif status == "retiring":
147 # We'll give them a few extra days over what we said
148 age = 6 * 31 * 24 * 60 * 60
150 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
158 #def IsGidDebian(account):
159 # return account['gidNumber'] == 800
161 # See if this user is in the group list
162 def IsInGroup(account, allowed, current_host):
163 # See if the primary group is in the list
164 if str(account['gidNumber']) in allowed: return True
166 # Check the host based ACL
167 if account.is_allowed_by_hostacl(current_host): return True
169 # See if there are supplementary groups
170 if not 'supplementaryGid' in account: return False
173 addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
175 if allowed.has_key(g):
179 def Die(File, F, Fdb):
185 os.remove(File + ".tmp")
189 os.remove(File + ".tdb.tmp")
193 def Done(File, F, Fdb):
196 os.rename(File + ".tmp", File)
199 os.rename(File + ".tdb.tmp", File + ".tdb")
201 # Generate the password list
202 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
205 F = open(File + ".tdb.tmp", "w")
210 # Do not let people try to buffer overflow some busted passwd parser.
211 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
213 userlist[a['uid']] = a['gidNumber']
214 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
220 HomePrefix, a['uid'],
222 line = Sanitize(line) + "\n"
223 F.write("0%u %s" % (i, line))
224 F.write(".%s %s" % (a['uid'], line))
225 F.write("=%d %s" % (a['uidNumber'], line))
228 # Oops, something unspeakable happened.
234 # Return the list of users so we know which keys to export
237 def GenAllUsers(accounts, file):
240 OldMask = os.umask(0022)
241 f = open(file + ".tmp", "w", 0644)
246 all.append( { 'uid': a['uid'],
247 'uidNumber': a['uidNumber'],
248 'active': a.pw_active() and a.shadow_active() } )
251 # Oops, something unspeakable happened.
257 # Generate the shadow list
258 def GenShadow(accounts, File):
261 OldMask = os.umask(0077)
262 F = open(File + ".tdb.tmp", "w", 0600)
267 # If the account is locked, mark it as such in shadow
268 # See Debian Bug #308229 for why we set it to 1 instead of 0
269 if not a.pw_active(): ShadowExpire = '1'
270 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
271 else: ShadowExpire = ''
274 values.append(a['uid'])
275 values.append(a.get_password())
276 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
277 if key in a: values.append(a[key])
278 else: values.append('')
279 values.append(ShadowExpire)
280 line = ':'.join(values)+':'
281 line = Sanitize(line) + "\n"
282 F.write("0%u %s" % (i, line))
283 F.write(".%s %s" % (a['uid'], line))
286 # Oops, something unspeakable happened.
292 # Generate the sudo passwd file
293 def GenShadowSudo(accounts, File, untrusted, current_host):
296 OldMask = os.umask(0077)
297 F = open(File + ".tmp", "w", 0600)
302 if 'sudoPassword' in a:
303 for entry in a['sudoPassword']:
304 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
307 uuid = Match.group(1)
308 status = Match.group(2)
309 hosts = Match.group(3)
310 cryptedpass = Match.group(4)
312 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
314 for_all = hosts == "*"
315 for_this_host = current_host in hosts.split(',')
316 if not (for_all or for_this_host):
318 # ignore * passwords for untrusted hosts, but copy host specific passwords
319 if for_all and untrusted:
322 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
327 Line = "%s:%s" % (a['uid'], Pass)
328 Line = Sanitize(Line) + "\n"
329 F.write("%s" % (Line))
331 # Oops, something unspeakable happened.
337 # Generate the sudo passwd file
338 def GenSSHGitolite(accounts, File):
341 OldMask = os.umask(0022)
342 F = open(File + ".tmp", "w", 0600)
345 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
347 if not 'sshRSAAuthKey' in a: continue
350 prefix = GitoliteSSHRestrictions.replace('@@USER@@', User)
351 for I in a["sshRSAAuthKey"]:
352 if I.startswith('ssh-'):
353 line = "%s %s"%(prefix, I)
355 line = "%s,%s"%(prefix, I)
356 line = Sanitize(line) + "\n"
359 # Oops, something unspeakable happened.
365 # Generate the shadow list
366 def GenSSHShadow(global_dir, accounts):
367 # Fetch all the users
371 if not 'sshRSAAuthKey' in a: continue
374 for I in a['sshRSAAuthKey']:
375 MultipleLine = "%s" % I
376 MultipleLine = Sanitize(MultipleLine)
377 contents.append(MultipleLine)
378 userkeys[a['uid']] = contents
381 # Generate the webPassword list
382 def GenWebPassword(accounts, File):
385 OldMask = os.umask(0077)
386 F = open(File, "w", 0600)
390 if not 'webPassword' in a: continue
391 if not a.pw_active(): continue
393 Pass = str(a['webPassword'])
394 Line = "%s:%s" % (a['uid'], Pass)
395 Line = Sanitize(Line) + "\n"
396 F.write("%s" % (Line))
402 # Generate the voipPassword list
403 def GenVoipPassword(accounts, File):
406 OldMask = os.umask(0077)
407 F = open(File, "w", 0600)
410 root = Element('domain')
411 root.attrib['name'] = "$${sip_profile}"
414 if not 'voipPassword' in a: continue
415 if not a.pw_active(): continue
417 Pass = str(a['voipPassword'])
418 user = Element('user')
419 user.attrib['id'] = "%s" % (a['uid'])
421 params = Element('params')
423 param = Element('param')
425 param.attrib['name'] = "a1-hash"
426 param.attrib['value'] = "%s" % (Pass)
428 F.write("%s" % (prettify(root)))
435 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
436 OldMask = os.umask(0077)
437 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
440 if f not in ssh_userkeys:
442 # If we're not exporting their primary group, don't export
445 if userlist[f] in grouprevmap.keys():
446 grname = grouprevmap[userlist[f]]
449 if int(userlist[f]) <= 100:
450 # In these cases, look it up in the normal way so we
451 # deal with cases where, for instance, users are in group
452 # users as their primary group.
453 grname = grp.getgrgid(userlist[f])[0]
458 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])
462 for line in ssh_userkeys[f]:
463 if line.startswith("allowed_hosts=") and ' ' in line:
464 machines, line = line.split('=', 1)[1].split(' ', 1)
465 if current_host not in machines.split(','):
466 continue # skip this key
469 continue # no keys for this host
470 contents = "\n".join(lines) + "\n"
472 to = tarfile.TarInfo(name=f)
473 # These will only be used where the username doesn't
474 # exist on the target system for some reason; hence,
475 # in those cases, the safest thing is for the file to
476 # be owned by root but group nobody. This deals with
477 # the bloody obscure case where the group fails to exist
478 # whilst the user does (in which case we want to avoid
479 # ending up with a file which is owned user:root to avoid
480 # a fairly obvious attack vector)
483 # Using the username / groupname fields avoids any need
484 # to give a shit^W^W^Wcare about the UIDoffset stuff.
488 to.mtime = int(time.time())
489 to.size = len(contents)
491 tf.addfile(to, StringIO(contents))
494 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
496 # add a list of groups to existing groups,
497 # including all subgroups thereof, recursively.
498 # basically this proceduces the transitive hull of the groups in
500 def addGroups(existingGroups, newGroups, uid, current_host):
501 for group in newGroups:
502 # if it's a <group>@host, split it and verify it's on the current host.
503 s = group.split('@', 1)
504 if len(s) == 2 and s[1] != current_host:
508 # let's see if we handled this group already
509 if group in existingGroups:
512 if not GroupIDMap.has_key(group):
513 print "Group", group, "does not exist but", uid, "is in it"
516 existingGroups.append(group)
518 if SubGroupMap.has_key(group):
519 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
521 # Generate the group list
522 def GenGroup(accounts, File, current_host):
526 F = open(File + ".tdb.tmp", "w")
528 # Generate the GroupMap
532 GroupHasPrimaryMembers = {}
534 # Sort them into a list of groups having a set of users
536 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
537 if not 'supplementaryGid' in a: continue
540 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
542 GroupMap[g].append(a['uid'])
544 # Output the group file.
546 for x in GroupMap.keys():
547 if not x in GroupIDMap:
550 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
553 grouprevmap[GroupIDMap[x]] = x
555 Line = "%s:x:%u:" % (x, GroupIDMap[x])
557 for I in GroupMap[x]:
558 Line = Line + ("%s%s" % (Comma, I))
560 Line = Sanitize(Line) + "\n"
561 F.write("0%u %s" % (J, Line))
562 F.write(".%s %s" % (x, Line))
563 F.write("=%u %s" % (GroupIDMap[x], Line))
566 # Oops, something unspeakable happened.
574 def CheckForward(accounts):
576 if not 'emailForward' in a: continue
580 # Do not allow people to try to buffer overflow busted parsers
581 if len(a['emailForward']) > 200: delete = True
582 # Check the forwarding address
583 elif EmailCheck.match(a['emailForward']) is None: delete = True
586 a.delete_mailforward()
588 # Generate the email forwarding list
589 def GenForward(accounts, File):
592 OldMask = os.umask(0022)
593 F = open(File + ".tmp", "w", 0644)
597 if not 'emailForward' in a: continue
598 Line = "%s: %s" % (a['uid'], a['emailForward'])
599 Line = Sanitize(Line) + "\n"
602 # Oops, something unspeakable happened.
608 def GenCDB(accounts, File, key):
611 OldMask = os.umask(0022)
612 # nothing else does the fsync stuff, so why do it here?
613 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
614 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
617 # Write out the email address for each user
619 if not key in a: continue
622 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
625 # Oops, something unspeakable happened.
629 if Fdb.close() != None:
630 raise "cdbmake gave an error"
632 # Generate the anon XEarth marker file
633 def GenMarkers(accounts, File):
636 F = open(File + ".tmp", "w")
638 # Write out the position for each user
640 if not ('latitude' in a and 'longitude' in a): continue
642 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
643 Line = Sanitize(Line) + "\n"
648 # Oops, something unspeakable happened.
654 # Generate the debian-private subscription list
655 def GenPrivate(accounts, File):
658 F = open(File + ".tmp", "w")
660 # Write out the position for each user
662 if not a.is_active_user(): continue
663 if not 'privateSub' in a: continue
665 Line = "%s"%(a['privateSub'])
666 Line = Sanitize(Line) + "\n"
671 # Oops, something unspeakable happened.
677 # Generate a list of locked accounts
678 def GenDisabledAccounts(accounts, File):
681 F = open(File + ".tmp", "w")
682 disabled_accounts = []
684 # Fetch all the users
686 if a.pw_active(): continue
687 Line = "%s:%s" % (a['uid'], "Account is locked")
688 disabled_accounts.append(a)
689 F.write(Sanitize(Line) + "\n")
691 # Oops, something unspeakable happened.
696 return disabled_accounts
698 # Generate the list of local addresses that refuse all mail
699 def GenMailDisable(accounts, File):
702 F = open(File + ".tmp", "w")
705 if not 'mailDisableMessage' in a: continue
706 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
707 Line = Sanitize(Line) + "\n"
710 # Oops, something unspeakable happened.
716 # Generate a list of uids that should have boolean affects applied
717 def GenMailBool(accounts, File, key):
720 F = open(File + ".tmp", "w")
723 if not key in a: continue
724 if not a[key] == 'TRUE': continue
725 Line = "%s"%(a['uid'])
726 Line = Sanitize(Line) + "\n"
729 # Oops, something unspeakable happened.
735 # Generate a list of hosts for RBL or whitelist purposes.
736 def GenMailList(accounts, File, key):
739 F = open(File + ".tmp", "w")
741 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
742 else: validregex = re.compile('^[-\w.]+$')
745 if not key in a: continue
747 filtered = filter(lambda z: validregex.match(z), a[key])
748 if len(filtered) == 0: continue
749 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
750 line = a['uid'] + ': ' + ' : '.join(filtered)
751 line = Sanitize(line) + "\n"
754 # Oops, something unspeakable happened.
760 def isRoleAccount(account):
761 return 'debianRoleAccount' in account['objectClass']
763 # Generate the DNS Zone file
764 def GenDNS(accounts, File):
767 F = open(File + ".tmp", "w")
769 # Fetch all the users
772 # Write out the zone file entry for each user
774 if not 'dnsZoneEntry' in a: continue
775 if not a.is_active_user() and not isRoleAccount(a): continue
778 F.write("; %s\n"%(a.email_address()))
779 for z in a["dnsZoneEntry"]:
780 Split = z.lower().split()
781 if Split[1].lower() == 'in':
782 Line = " ".join(Split) + "\n"
785 Host = Split[0] + DNSZone
786 if BSMTPCheck.match(Line) != None:
787 F.write("; Has BSMTP\n")
789 # Write some identification information
790 if not RRs.has_key(Host):
791 if Split[2].lower() in ["a", "aaaa"]:
792 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
793 for y in a["keyFingerPrint"]:
794 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
798 Line = "; Err %s"%(str(Split))
803 F.write("; Errors:\n")
804 for line in str(e).split("\n"):
805 F.write("; %s\n"%(line))
808 # Oops, something unspeakable happened.
814 def ExtractDNSInfo(x):
818 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
821 if x[1].has_key("ipHostNumber"):
822 for I in x[1]["ipHostNumber"]:
823 if IsV6Addr.match(I) != None:
824 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
826 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
830 if 'sshRSAHostKey' in x[1]:
831 for I in x[1]["sshRSAHostKey"]:
833 if Split[0] == 'ssh-rsa':
835 if Split[0] == 'ssh-dss':
837 if Algorithm == None:
839 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
840 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
842 if 'architecture' in x[1]:
843 Arch = GetAttr(x, "architecture")
845 if x[1].has_key("machine"):
846 Mach = " " + GetAttr(x, "machine")
847 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
849 if x[1].has_key("mXRecord"):
850 for I in x[1]["mXRecord"]:
851 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
855 # Generate the DNS records
856 def GenZoneRecords(host_attrs, File):
859 F = open(File + ".tmp", "w")
861 # Fetch all the hosts
863 if x[1].has_key("hostname") == 0:
866 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
869 DNSInfo = ExtractDNSInfo(x)
873 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
876 Line = "\t\t\t%s" % (Line)
880 # this would write sshfp lines for services on machines
881 # but we can't yet, since some are cnames and we'll make
882 # an invalid zonefile
884 # for i in x[1].get("purpose", []):
885 # m = PurposeHostField.match(i)
888 # # we ignore [[*..]] entries
889 # if m.startswith('*'):
891 # if m.startswith('-'):
894 # if not m.endswith(HostDomain):
896 # if not m.endswith('.'):
898 # for Line in DNSInfo:
899 # if isSSHFP.match(Line):
900 # Line = "%s\t%s" % (m, Line)
901 # F.write(Line + "\n")
903 # Oops, something unspeakable happened.
909 # Generate the BSMTP file
910 def GenBSMTP(accounts, File, HomePrefix):
913 F = open(File + ".tmp", "w")
915 # Write out the zone file entry for each user
917 if not 'dnsZoneEntry' in a: continue
918 if not a.is_active_user(): continue
921 for z in a["dnsZoneEntry"]:
922 Split = z.lower().split()
923 if Split[1].lower() == 'in':
924 for y in range(0, len(Split)):
927 Line = " ".join(Split) + "\n"
929 Host = Split[0] + DNSZone
930 if BSMTPCheck.match(Line) != None:
931 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
932 a['uid'], HomePrefix, a['uid'], Host))
935 F.write("; Errors\n")
938 # Oops, something unspeakable happened.
944 def HostToIP(Host, mapped=True):
948 if Host[1].has_key("ipHostNumber"):
949 for addr in Host[1]["ipHostNumber"]:
950 IPAdresses.append(addr)
951 if IsV6Addr.match(addr) is None and mapped == "True":
952 IPAdresses.append("::ffff:"+addr)
956 # Generate the ssh known hosts file
957 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
960 OldMask = os.umask(0022)
961 F = open(File + ".tmp", "w", 0644)
965 if x[1].has_key("hostname") == 0 or \
966 x[1].has_key("sshRSAHostKey") == 0:
968 Host = GetAttr(x, "hostname")
970 if Host.endswith(HostDomain):
971 HostNames.append(Host[:-(len(HostDomain) + 1)])
973 # in the purpose field [[host|some other text]] (where some other text is optional)
974 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
975 # file. But so that we don't have to add everything we link we can add an asterisk
976 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
977 # http linking it we also support [[-hostname]] entries.
978 for i in x[1].get("purpose", []):
979 m = PurposeHostField.match(i)
982 # we ignore [[*..]] entries
983 if m.startswith('*'):
985 if m.startswith('-'):
989 if m.endswith(HostDomain):
990 HostNames.append(m[:-(len(HostDomain) + 1)])
992 for I in x[1]["sshRSAHostKey"]:
993 if mode and mode == 'authorized_keys':
995 if 'sshdistAuthKeysHost' in x[1]:
996 hosts += x[1]['sshdistAuthKeysHost']
997 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
998 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
999 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1001 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1002 Line = Sanitize(Line) + "\n"
1004 # Oops, something unspeakable happened.
1010 # Generate the debianhosts file (list of all IP addresses)
1011 def GenHosts(host_attrs, File):
1014 OldMask = os.umask(0022)
1015 F = open(File + ".tmp", "w", 0644)
1020 for x in host_attrs:
1022 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1025 if not 'ipHostNumber' in x[1]:
1028 addrs = x[1]["ipHostNumber"]
1030 if addr not in seen:
1032 addr = Sanitize(addr) + "\n"
1035 # Oops, something unspeakable happened.
1041 def replaceTree(src, dst_basedir):
1042 bn = os.path.basename(src)
1043 dst = os.path.join(dst_basedir, bn)
1045 shutil.copytree(src, dst)
1047 def GenKeyrings(OutDir):
1049 if os.path.isdir(k):
1050 replaceTree(k, OutDir)
1052 shutil.copy(k, OutDir)
1055 def get_accounts(ldap_conn):
1056 # Fetch all the users
1057 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1058 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1059 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1060 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1061 "shadowExpire", "emailForward", "latitude", "longitude",\
1062 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1063 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1064 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1065 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1066 "mailContentInspectionAction", "webPassword", "voipPassword"])
1068 if passwd_attrs is None:
1069 raise UDEmptyList, "No Users"
1070 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1071 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1075 def get_hosts(ldap_conn):
1076 # Fetch all the hosts
1077 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1078 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1079 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1081 if HostAttrs == None:
1082 raise UDEmptyList, "No Hosts"
1084 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1089 def make_ldap_conn():
1090 # Connect to the ldap server
1092 # for testing purposes it's sometimes useful to pass username/password
1093 # via the environment
1094 if 'UD_CREDENTIALS' in os.environ:
1095 Pass = os.environ['UD_CREDENTIALS'].split()
1097 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1098 Pass = F.readline().strip().split(" ")
1100 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1106 def setup_group_maps(l):
1107 # Fetch all the groups
1110 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1111 ["gid", "gidNumber", "subGroup"])
1113 # Generate the subgroup_map and group_id_map
1115 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1117 if x[1].has_key("gidNumber") == 0:
1119 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1120 if x[1].has_key("subGroup") != 0:
1121 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1125 SubGroupMap = subgroup_map
1126 GroupIDMap = group_id_map
1128 def generate_all(global_dir, ldap_conn):
1129 accounts = get_accounts(ldap_conn)
1130 host_attrs = get_hosts(ldap_conn)
1133 # Generate global things
1134 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1136 accounts = filter(lambda x: not IsRetired(x), accounts)
1137 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1139 CheckForward(accounts)
1141 GenMailDisable(accounts, global_dir + "mail-disable")
1142 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1143 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1144 GenPrivate(accounts, global_dir + "debian-private")
1145 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1146 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1147 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1148 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1149 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1150 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1151 GenWebPassword(accounts, global_dir + "web-passwords")
1152 GenVoipPassword(accounts, global_dir + "voip-passwords")
1153 GenKeyrings(global_dir)
1156 GenForward(accounts, global_dir + "forward-alias")
1158 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1159 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1161 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1162 GenMarkers(accounts, global_dir + "markers")
1163 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1164 GenHosts(host_attrs, global_dir + "debianhosts")
1165 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1167 GenDNS(accounts, global_dir + "dns-zone")
1168 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1170 setup_group_maps(ldap_conn)
1172 for host in host_attrs:
1173 if not "hostname" in host[1]:
1175 generate_host(host, global_dir, accounts, ssh_userkeys)
1177 def generate_host(host, global_dir, accounts, ssh_userkeys):
1178 current_host = host[1]['hostname'][0]
1179 OutDir = global_dir + current_host + '/'
1180 if not os.path.isdir(OutDir):
1183 # Get the group list and convert any named groups to numerics
1185 for groupname in AllowedGroupsPreload.strip().split(" "):
1186 GroupList[groupname] = True
1187 if 'allowedGroups' in host[1]:
1188 for groupname in host[1]['allowedGroups']:
1189 GroupList[groupname] = True
1190 for groupname in GroupList.keys():
1191 if groupname in GroupIDMap:
1192 GroupList[str(GroupIDMap[groupname])] = True
1195 if 'exportOptions' in host[1]:
1196 for extra in host[1]['exportOptions']:
1197 ExtraList[extra.upper()] = True
1200 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), accounts)
1202 DoLink(global_dir, OutDir, "debianhosts")
1203 DoLink(global_dir, OutDir, "ssh_known_hosts")
1204 DoLink(global_dir, OutDir, "disabled-accounts")
1207 if 'NOPASSWD' in ExtraList:
1208 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1210 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1212 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1213 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1215 # Now we know who we're allowing on the machine, export
1216 # the relevant ssh keys
1217 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1219 if not 'NOPASSWD' in ExtraList:
1220 GenShadow(accounts, OutDir + "shadow")
1222 # Link in global things
1223 if not 'NOMARKERS' in ExtraList:
1224 DoLink(global_dir, OutDir, "markers")
1225 DoLink(global_dir, OutDir, "mail-forward.cdb")
1226 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1227 DoLink(global_dir, OutDir, "mail-disable")
1228 DoLink(global_dir, OutDir, "mail-greylist")
1229 DoLink(global_dir, OutDir, "mail-callout")
1230 DoLink(global_dir, OutDir, "mail-rbl")
1231 DoLink(global_dir, OutDir, "mail-rhsbl")
1232 DoLink(global_dir, OutDir, "mail-whitelist")
1233 DoLink(global_dir, OutDir, "all-accounts.json")
1234 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1235 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1236 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1239 DoLink(global_dir, OutDir, "forward-alias")
1241 if 'DNS' in ExtraList:
1242 DoLink(global_dir, OutDir, "dns-zone")
1243 DoLink(global_dir, OutDir, "dns-sshfp")
1245 if 'AUTHKEYS' in ExtraList:
1246 DoLink(global_dir, OutDir, "authorized_keys")
1248 if 'BSMTP' in ExtraList:
1249 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1251 if 'PRIVATE' in ExtraList:
1252 DoLink(global_dir, OutDir, "debian-private")
1254 if 'GITOLITE' in ExtraList:
1255 DoLink(global_dir, OutDir, "ssh-gitolite")
1257 if 'WEB-PASSWORDS' in ExtraList:
1258 DoLink(global_dir, OutDir, "web-passwords")
1260 if 'VOIP-PASSWORDS' in ExtraList:
1261 DoLink(global_dir, OutDir, "voip-passwords")
1263 if 'KEYRING' in ExtraList:
1265 bn = os.path.basename(k)
1266 if os.path.isdir(k):
1267 src = os.path.join(global_dir, bn)
1268 replaceTree(src, OutDir)
1270 DoLink(global_dir, OutDir, bn)
1274 bn = os.path.basename(k)
1275 target = os.path.join(OutDir, bn)
1276 if os.path.isdir(target):
1279 posix.remove(target)
1282 DoLink(global_dir, OutDir, "last_update.trace")
1285 def getLastLDAPChangeTime(l):
1286 mods = l.search_s('cn=log',
1287 ldap.SCOPE_ONELEVEL,
1288 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1293 # Sort the list by reqEnd
1294 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1295 # Take the last element in the array
1296 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1300 def getLastKeyringChangeTime():
1303 mt = os.path.getmtime(k)
1309 def getLastBuildTime(gdir):
1310 cache_last_ldap_mod = 0
1311 cache_last_unix_mod = 0
1314 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1315 cache_last_mod=fd.read().split()
1317 cache_last_ldap_mod = cache_last_mod[0]
1318 cache_last_unix_mod = int(cache_last_mod[1])
1319 except IndexError, ValueError:
1323 if e.errno == errno.ENOENT:
1328 return (cache_last_ldap_mod, cache_last_unix_mod)
1331 parser = optparse.OptionParser()
1332 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1333 help="Output directory.")
1334 parser.add_option("-f", "--force", dest="force", action="store_true",
1335 help="Force generation, even if not update to LDAP has happened.")
1337 (options, args) = parser.parse_args()
1342 if options.generatedir is not None:
1343 generate_dir = os.environ['UD_GENERATEDIR']
1344 elif 'UD_GENERATEDIR' in os.environ:
1345 generate_dir = os.environ['UD_GENERATEDIR']
1347 generate_dir = GenerateDir
1350 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1351 lock = get_lock( lockf )
1353 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1356 l = make_ldap_conn()
1358 time_started = int(time.time())
1359 ldap_last_mod = getLastLDAPChangeTime(l)
1360 unix_last_mod = getLastKeyringChangeTime()
1361 cache_last_ldap_mod, cache_last_unix_mod = getLastBuildTime(generate_dir)
1363 need_update = (ldap_last_mod > cache_last_ldap_mod) or (unix_last_mod > cache_last_unix_mod)
1365 if not options.force and not need_update:
1366 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1367 fd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1371 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1372 generate_all(generate_dir, l)
1373 tracefd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1377 if __name__ == "__main__":
1378 if 'UD_PROFILE' in os.environ:
1381 cProfile.run('ud_generate()', "udg_prof")
1382 p = pstats.Stats('udg_prof')
1383 ##p.sort_stats('time').print_stats()
1384 p.sort_stats('cumulative').print_stats()
1390 # vim:set shiftwidth=3: