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, dbm
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}'
66 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)(,\s*([^ <>@]+@[^ ,<>@]+))*$")
67 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
68 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-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)
74 GitoliteExportHosts = re.compile(getattr(ConfModule, "gitoliteexporthosts", "."))
75 MX_remap = json.loads(ConfModule.MX_remap)
78 """Return a pretty-printed XML string for the Element.
80 rough_string = ElementTree.tostring(elem, 'utf-8')
81 reparsed = minidom.parseString(rough_string)
82 return reparsed.toprettyxml(indent=" ")
84 def safe_makedirs(dir):
88 if e.errno == errno.EEXIST:
97 if e.errno == errno.ENOENT:
102 def get_lock(fn, wait=5*60):
105 ends = time.time() + wait
110 fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
114 if time.time() >= ends:
116 sl = min(sl*2, 10, ends - time.time())
122 return Str.translate(string.maketrans("\n\r\t", "$$$"))
124 def DoLink(From, To, File):
126 posix.remove(To + File)
129 posix.link(From + File, To + File)
131 def IsRetired(account):
133 Looks for accountStatus in the LDAP record and tries to
134 match it against one of the known retired statuses
137 status = account['accountStatus']
139 line = status.split()
142 if status == "inactive":
145 elif status == "memorial":
148 elif status == "retiring":
149 # We'll give them a few extra days over what we said
150 age = 6 * 31 * 24 * 60 * 60
152 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
160 #def IsGidDebian(account):
161 # return account['gidNumber'] == 800
163 # See if this user is in the group list
164 def IsInGroup(account, allowed, current_host):
165 # See if the primary group is in the list
166 if str(account['gidNumber']) in allowed: return True
168 # Check the host based ACL
169 if account.is_allowed_by_hostacl(current_host): return True
171 # See if there are supplementary groups
172 if not 'supplementaryGid' in account: return False
175 addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
181 def Die(File, F, Fdb):
187 os.remove(File + ".tmp")
191 os.remove(File + ".tdb.tmp")
195 def Done(File, F, Fdb):
198 os.rename(File + ".tmp", File)
201 os.rename(File + ".tdb.tmp", File + ".tdb")
203 # Generate the password list
204 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
207 F = open(File + ".tdb.tmp", "w")
212 # Do not let people try to buffer overflow some busted passwd parser.
213 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
215 userlist[a['uid']] = a['gidNumber']
216 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
222 HomePrefix, a['uid'],
224 line = Sanitize(line) + "\n"
225 F.write("0%u %s" % (i, line))
226 F.write(".%s %s" % (a['uid'], line))
227 F.write("=%d %s" % (a['uidNumber'], line))
230 # Oops, something unspeakable happened.
236 # Return the list of users so we know which keys to export
239 def GenAllUsers(accounts, file):
242 OldMask = os.umask(0022)
243 f = open(file + ".tmp", "w", 0644)
248 all.append( { 'uid': a['uid'],
249 'uidNumber': a['uidNumber'],
250 'active': a.pw_active() and a.shadow_active() } )
253 # Oops, something unspeakable happened.
259 # Generate the shadow list
260 def GenShadow(accounts, File):
263 OldMask = os.umask(0077)
264 F = open(File + ".tdb.tmp", "w", 0600)
269 # If the account is locked, mark it as such in shadow
270 # See Debian Bug #308229 for why we set it to 1 instead of 0
271 if not a.pw_active(): ShadowExpire = '1'
272 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
273 else: ShadowExpire = ''
276 values.append(a['uid'])
277 values.append(a.get_password())
278 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
279 if key in a: values.append(a[key])
280 else: values.append('')
281 values.append(ShadowExpire)
282 line = ':'.join(values)+':'
283 line = Sanitize(line) + "\n"
284 F.write("0%u %s" % (i, line))
285 F.write(".%s %s" % (a['uid'], line))
288 # Oops, something unspeakable happened.
294 # Generate the sudo passwd file
295 def GenShadowSudo(accounts, File, untrusted, current_host):
298 OldMask = os.umask(0077)
299 F = open(File + ".tmp", "w", 0600)
304 if 'sudoPassword' in a:
305 for entry in a['sudoPassword']:
306 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
309 uuid = Match.group(1)
310 status = Match.group(2)
311 hosts = Match.group(3)
312 cryptedpass = Match.group(4)
314 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
316 for_all = hosts == "*"
317 for_this_host = current_host in hosts.split(',')
318 if not (for_all or for_this_host):
320 # ignore * passwords for untrusted hosts, but copy host specific passwords
321 if for_all and untrusted:
324 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
329 Line = "%s:%s" % (a['uid'], Pass)
330 Line = Sanitize(Line) + "\n"
331 F.write("%s" % (Line))
333 # Oops, something unspeakable happened.
339 # Generate the sudo passwd file
340 def GenSSHGitolite(accounts, hosts, File):
343 OldMask = os.umask(0022)
344 F = open(File + ".tmp", "w", 0600)
347 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
349 if not 'sshRSAAuthKey' in a: continue
352 prefix = GitoliteSSHRestrictions.replace('@@USER@@', User)
353 for I in a["sshRSAAuthKey"]:
354 if I.startswith('ssh-'):
355 line = "%s %s"%(prefix, I)
357 line = "%s,%s"%(prefix, I)
358 line = Sanitize(line) + "\n"
361 for dn, attrs in hosts:
362 if not 'sshRSAHostKey' in attrs: continue
363 hostname = "host-" + attrs['hostname'][0]
364 prefix = GitoliteSSHRestrictions.replace('@@USER@@', hostname)
365 for I in attrs["sshRSAHostKey"]:
366 line = "%s %s"%(prefix, I)
367 line = Sanitize(line) + "\n"
370 # Oops, something unspeakable happened.
376 # Generate the shadow list
377 def GenSSHShadow(global_dir, accounts):
378 # Fetch all the users
382 if not 'sshRSAAuthKey' in a: continue
385 for I in a['sshRSAAuthKey']:
386 MultipleLine = "%s" % I
387 MultipleLine = Sanitize(MultipleLine)
388 contents.append(MultipleLine)
389 userkeys[a['uid']] = contents
392 # Generate the webPassword list
393 def GenWebPassword(accounts, File):
396 OldMask = os.umask(0077)
397 F = open(File, "w", 0600)
401 if not 'webPassword' in a: continue
402 if not a.pw_active(): continue
404 Pass = str(a['webPassword'])
405 Line = "%s:%s" % (a['uid'], Pass)
406 Line = Sanitize(Line) + "\n"
407 F.write("%s" % (Line))
413 # Generate the voipPassword list
414 def GenVoipPassword(accounts, File):
417 OldMask = os.umask(0077)
418 F = open(File, "w", 0600)
421 root = Element('include')
424 if not 'voipPassword' in a: continue
425 if not a.pw_active(): continue
427 Pass = str(a['voipPassword'])
428 user = Element('user')
429 user.attrib['id'] = "%s" % (a['uid'])
431 params = Element('params')
433 param = Element('param')
435 param.attrib['name'] = "a1-hash"
436 param.attrib['value'] = "%s" % (Pass)
437 variables = Element('variables')
438 user.append(variables)
439 variable = Element('variable')
440 variable.attrib['name'] = "toll_allow"
441 variable.attrib['value'] = "domestic,international,local"
442 variables.append(variable)
444 F.write("%s" % (prettify(root)))
451 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
452 OldMask = os.umask(0077)
453 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
456 if f not in ssh_userkeys:
458 # If we're not exporting their primary group, don't export
461 if userlist[f] in grouprevmap.keys():
462 grname = grouprevmap[userlist[f]]
465 if int(userlist[f]) <= 100:
466 # In these cases, look it up in the normal way so we
467 # deal with cases where, for instance, users are in group
468 # users as their primary group.
469 grname = grp.getgrgid(userlist[f])[0]
474 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])
478 for line in ssh_userkeys[f]:
479 if line.startswith("allowed_hosts=") and ' ' in line:
480 machines, line = line.split('=', 1)[1].split(' ', 1)
481 if current_host not in machines.split(','):
482 continue # skip this key
485 continue # no keys for this host
486 contents = "\n".join(lines) + "\n"
488 to = tarfile.TarInfo(name=f)
489 # These will only be used where the username doesn't
490 # exist on the target system for some reason; hence,
491 # in those cases, the safest thing is for the file to
492 # be owned by root but group nobody. This deals with
493 # the bloody obscure case where the group fails to exist
494 # whilst the user does (in which case we want to avoid
495 # ending up with a file which is owned user:root to avoid
496 # a fairly obvious attack vector)
499 # Using the username / groupname fields avoids any need
500 # to give a shit^W^W^Wcare about the UIDoffset stuff.
504 to.mtime = int(time.time())
505 to.size = len(contents)
507 tf.addfile(to, StringIO(contents))
510 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
512 # add a list of groups to existing groups,
513 # including all subgroups thereof, recursively.
514 # basically this proceduces the transitive hull of the groups in
516 def addGroups(existingGroups, newGroups, uid, current_host):
517 for group in newGroups:
518 # if it's a <group>@host, split it and verify it's on the current host.
519 s = group.split('@', 1)
520 if len(s) == 2 and s[1] != current_host:
524 # let's see if we handled this group already
525 if group in existingGroups:
528 if not GroupIDMap.has_key(group):
529 print "Group", group, "does not exist but", uid, "is in it"
532 existingGroups.append(group)
534 if SubGroupMap.has_key(group):
535 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
537 # Generate the group list
538 def GenGroup(accounts, File, current_host):
542 F = open(File + ".tdb.tmp", "w")
544 # Generate the GroupMap
548 GroupHasPrimaryMembers = {}
550 # Sort them into a list of groups having a set of users
552 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
553 if not 'supplementaryGid' in a: continue
556 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
558 GroupMap[g].append(a['uid'])
560 # Output the group file.
562 for x in GroupMap.keys():
563 if not x in GroupIDMap:
566 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
569 grouprevmap[GroupIDMap[x]] = x
571 Line = "%s:x:%u:" % (x, GroupIDMap[x])
573 for I in GroupMap[x]:
574 Line = Line + ("%s%s" % (Comma, I))
576 Line = Sanitize(Line) + "\n"
577 F.write("0%u %s" % (J, Line))
578 F.write(".%s %s" % (x, Line))
579 F.write("=%u %s" % (GroupIDMap[x], Line))
582 # Oops, something unspeakable happened.
590 def CheckForward(accounts):
592 if not 'emailForward' in a: continue
596 # Do not allow people to try to buffer overflow busted parsers
597 if len(a['emailForward']) > 200: delete = True
598 # Check the forwarding address
599 elif EmailCheck.match(a['emailForward']) is None: delete = True
602 a.delete_mailforward()
604 # Generate the email forwarding list
605 def GenForward(accounts, File):
608 OldMask = os.umask(0022)
609 F = open(File + ".tmp", "w", 0644)
613 if not 'emailForward' in a: continue
614 Line = "%s: %s" % (a['uid'], a['emailForward'])
615 Line = Sanitize(Line) + "\n"
618 # Oops, something unspeakable happened.
624 def GenCDB(accounts, File, key):
627 OldMask = os.umask(0022)
628 # nothing else does the fsync stuff, so why do it here?
629 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
630 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
633 # Write out the email address for each user
635 if not key in a: continue
638 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
641 # Oops, something unspeakable happened.
645 if Fdb.close() != None:
646 raise "cdbmake gave an error"
648 def GenDBM(accounts, File, key):
650 OldMask = os.umask(0022)
651 fn = os.path.join(File).encode('ascii', 'ignore')
658 Fdb = dbm.open(fn, "c")
661 # Write out the email address for each user
663 if not key in a: continue
670 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
671 os.remove(File + ".db")
673 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
674 os.rename (File + ".db", File)
676 # Generate the anon XEarth marker file
677 def GenMarkers(accounts, File):
680 F = open(File + ".tmp", "w")
682 # Write out the position for each user
684 if not ('latitude' in a and 'longitude' in a): continue
686 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
687 Line = Sanitize(Line) + "\n"
692 # Oops, something unspeakable happened.
698 # Generate the debian-private subscription list
699 def GenPrivate(accounts, File):
702 F = open(File + ".tmp", "w")
704 # Write out the position for each user
706 if not a.is_active_user(): continue
707 if a.is_guest_account(): continue
708 if not 'privateSub' in a: continue
710 Line = "%s"%(a['privateSub'])
711 Line = Sanitize(Line) + "\n"
716 # Oops, something unspeakable happened.
722 # Generate a list of locked accounts
723 def GenDisabledAccounts(accounts, File):
726 F = open(File + ".tmp", "w")
727 disabled_accounts = []
729 # Fetch all the users
731 if a.pw_active(): continue
732 Line = "%s:%s" % (a['uid'], "Account is locked")
733 disabled_accounts.append(a)
734 F.write(Sanitize(Line) + "\n")
736 # Oops, something unspeakable happened.
741 return disabled_accounts
743 # Generate the list of local addresses that refuse all mail
744 def GenMailDisable(accounts, File):
747 F = open(File + ".tmp", "w")
750 if not 'mailDisableMessage' in a: continue
751 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
752 Line = Sanitize(Line) + "\n"
755 # Oops, something unspeakable happened.
761 # Generate a list of uids that should have boolean affects applied
762 def GenMailBool(accounts, File, key):
765 F = open(File + ".tmp", "w")
768 if not key in a: continue
769 if not a[key] == 'TRUE': continue
770 Line = "%s"%(a['uid'])
771 Line = Sanitize(Line) + "\n"
774 # Oops, something unspeakable happened.
780 # Generate a list of hosts for RBL or whitelist purposes.
781 def GenMailList(accounts, File, key):
784 F = open(File + ".tmp", "w")
786 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
787 else: validregex = re.compile('^[-\w.]+$')
790 if not key in a: continue
792 filtered = filter(lambda z: validregex.match(z), a[key])
793 if len(filtered) == 0: continue
794 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
795 line = a['uid'] + ': ' + ' : '.join(filtered)
796 line = Sanitize(line) + "\n"
799 # Oops, something unspeakable happened.
805 def isRoleAccount(account):
806 return 'debianRoleAccount' in account['objectClass']
808 # Generate the DNS Zone file
809 def GenDNS(accounts, File):
812 F = open(File + ".tmp", "w")
814 # Fetch all the users
817 # Write out the zone file entry for each user
819 if not 'dnsZoneEntry' in a: continue
820 if not a.is_active_user() and not isRoleAccount(a): continue
821 if a.is_guest_account(): continue
824 F.write("; %s\n"%(a.email_address()))
825 for z in a["dnsZoneEntry"]:
826 Split = z.lower().split()
827 if Split[1].lower() == 'in':
828 Line = " ".join(Split) + "\n"
831 Host = Split[0] + DNSZone
832 if BSMTPCheck.match(Line) != None:
833 F.write("; Has BSMTP\n")
835 # Write some identification information
836 if not RRs.has_key(Host):
837 if Split[2].lower() in ["a", "aaaa"]:
838 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
839 for y in a["keyFingerPrint"]:
840 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
844 Line = "; Err %s"%(str(Split))
849 F.write("; Errors:\n")
850 for line in str(e).split("\n"):
851 F.write("; %s\n"%(line))
854 # Oops, something unspeakable happened.
862 socket.inet_pton(socket.AF_INET6, i)
867 def ExtractDNSInfo(x):
871 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
874 if x[1].has_key("ipHostNumber"):
875 for I in x[1]["ipHostNumber"]:
877 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
879 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
883 if 'sshRSAHostKey' in x[1]:
884 for I in x[1]["sshRSAHostKey"]:
886 if Split[0] == 'ssh-rsa':
888 if Split[0] == 'ssh-dss':
890 if Algorithm == None:
892 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
893 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
895 if 'architecture' in x[1]:
896 Arch = GetAttr(x, "architecture")
898 if x[1].has_key("machine"):
899 Mach = " " + GetAttr(x, "machine")
900 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
902 if x[1].has_key("mXRecord"):
903 for I in x[1]["mXRecord"]:
905 for e in MX_remap[I]:
906 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
908 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
912 # Generate the DNS records
913 def GenZoneRecords(host_attrs, File):
916 F = open(File + ".tmp", "w")
918 # Fetch all the hosts
920 if x[1].has_key("hostname") == 0:
923 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
926 DNSInfo = ExtractDNSInfo(x)
930 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
933 Line = "\t\t\t%s" % (Line)
937 # this would write sshfp lines for services on machines
938 # but we can't yet, since some are cnames and we'll make
939 # an invalid zonefile
941 # for i in x[1].get("purpose", []):
942 # m = PurposeHostField.match(i)
945 # # we ignore [[*..]] entries
946 # if m.startswith('*'):
948 # if m.startswith('-'):
951 # if not m.endswith(HostDomain):
953 # if not m.endswith('.'):
955 # for Line in DNSInfo:
956 # if isSSHFP.match(Line):
957 # Line = "%s\t%s" % (m, Line)
958 # F.write(Line + "\n")
960 # Oops, something unspeakable happened.
966 # Generate the BSMTP file
967 def GenBSMTP(accounts, File, HomePrefix):
970 F = open(File + ".tmp", "w")
972 # Write out the zone file entry for each user
974 if not 'dnsZoneEntry' in a: continue
975 if not a.is_active_user(): continue
978 for z in a["dnsZoneEntry"]:
979 Split = z.lower().split()
980 if Split[1].lower() == 'in':
981 for y in range(0, len(Split)):
984 Line = " ".join(Split) + "\n"
986 Host = Split[0] + DNSZone
987 if BSMTPCheck.match(Line) != None:
988 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
989 a['uid'], HomePrefix, a['uid'], Host))
992 F.write("; Errors\n")
995 # Oops, something unspeakable happened.
1001 def HostToIP(Host, mapped=True):
1005 if Host[1].has_key("ipHostNumber"):
1006 for addr in Host[1]["ipHostNumber"]:
1007 IPAdresses.append(addr)
1008 if not is_ipv6_addr(addr) and mapped == "True":
1009 IPAdresses.append("::ffff:"+addr)
1013 # Generate the ssh known hosts file
1014 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
1017 OldMask = os.umask(0022)
1018 F = open(File + ".tmp", "w", 0644)
1021 for x in host_attrs:
1022 if x[1].has_key("hostname") == 0 or \
1023 x[1].has_key("sshRSAHostKey") == 0:
1025 Host = GetAttr(x, "hostname")
1026 HostNames = [ Host ]
1027 if Host.endswith(HostDomain):
1028 HostNames.append(Host[:-(len(HostDomain) + 1)])
1030 # in the purpose field [[host|some other text]] (where some other text is optional)
1031 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1032 # file. But so that we don't have to add everything we link we can add an asterisk
1033 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
1034 # http linking it we also support [[-hostname]] entries.
1035 for i in x[1].get("purpose", []):
1036 m = PurposeHostField.match(i)
1039 # we ignore [[*..]] entries
1040 if m.startswith('*'):
1042 if m.startswith('-'):
1046 if m.endswith(HostDomain):
1047 HostNames.append(m[:-(len(HostDomain) + 1)])
1049 for I in x[1]["sshRSAHostKey"]:
1050 if mode and mode == 'authorized_keys':
1052 if 'sshdistAuthKeysHost' in x[1]:
1053 hosts += x[1]['sshdistAuthKeysHost']
1054 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1055 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1056 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1058 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1059 Line = Sanitize(Line) + "\n"
1061 # Oops, something unspeakable happened.
1067 # Generate the debianhosts file (list of all IP addresses)
1068 def GenHosts(host_attrs, File):
1071 OldMask = os.umask(0022)
1072 F = open(File + ".tmp", "w", 0644)
1077 for x in host_attrs:
1079 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1082 if not 'ipHostNumber' in x[1]:
1085 addrs = x[1]["ipHostNumber"]
1087 if addr not in seen:
1089 addr = Sanitize(addr) + "\n"
1092 # Oops, something unspeakable happened.
1098 def replaceTree(src, dst_basedir):
1099 bn = os.path.basename(src)
1100 dst = os.path.join(dst_basedir, bn)
1102 shutil.copytree(src, dst)
1104 def GenKeyrings(OutDir):
1106 if os.path.isdir(k):
1107 replaceTree(k, OutDir)
1109 shutil.copy(k, OutDir)
1112 def get_accounts(ldap_conn):
1113 # Fetch all the users
1114 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1115 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1116 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1117 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1118 "shadowExpire", "emailForward", "latitude", "longitude",\
1119 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1120 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1121 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1122 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1123 "mailContentInspectionAction", "webPassword", "voipPassword"])
1125 if passwd_attrs is None:
1126 raise UDEmptyList, "No Users"
1127 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1128 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1132 def get_hosts(ldap_conn):
1133 # Fetch all the hosts
1134 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1135 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1136 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1138 if HostAttrs == None:
1139 raise UDEmptyList, "No Hosts"
1141 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1146 def make_ldap_conn():
1147 # Connect to the ldap server
1149 # for testing purposes it's sometimes useful to pass username/password
1150 # via the environment
1151 if 'UD_CREDENTIALS' in os.environ:
1152 Pass = os.environ['UD_CREDENTIALS'].split()
1154 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1155 Pass = F.readline().strip().split(" ")
1157 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1163 def setup_group_maps(l):
1164 # Fetch all the groups
1167 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1168 ["gid", "gidNumber", "subGroup"])
1170 # Generate the subgroup_map and group_id_map
1172 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1174 if x[1].has_key("gidNumber") == 0:
1176 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1177 if x[1].has_key("subGroup") != 0:
1178 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1182 SubGroupMap = subgroup_map
1183 GroupIDMap = group_id_map
1185 def generate_all(global_dir, ldap_conn):
1186 accounts = get_accounts(ldap_conn)
1187 host_attrs = get_hosts(ldap_conn)
1190 # Generate global things
1191 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1193 accounts = filter(lambda x: not IsRetired(x), accounts)
1194 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1196 CheckForward(accounts)
1198 GenMailDisable(accounts, global_dir + "mail-disable")
1199 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1200 GenDBM(accounts, global_dir + "mail-forward.db", 'emailForward')
1201 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1202 GenDBM(accounts, global_dir + "mail-contentinspectionaction.db", 'mailContentInspectionAction')
1203 GenPrivate(accounts, global_dir + "debian-private")
1204 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1205 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1206 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1207 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1208 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1209 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1210 GenWebPassword(accounts, global_dir + "web-passwords")
1211 GenVoipPassword(accounts, global_dir + "voip-passwords")
1212 GenKeyrings(global_dir)
1215 GenForward(accounts, global_dir + "forward-alias")
1217 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1218 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1220 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1221 GenMarkers(accounts, global_dir + "markers")
1222 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1223 GenHosts(host_attrs, global_dir + "debianhosts")
1224 GenSSHGitolite(accounts, host_attrs, global_dir + "ssh-gitolite")
1226 GenDNS(accounts, global_dir + "dns-zone")
1227 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1229 setup_group_maps(ldap_conn)
1231 for host in host_attrs:
1232 if not "hostname" in host[1]:
1234 generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1236 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1237 current_host = host[1]['hostname'][0]
1238 OutDir = global_dir + current_host + '/'
1239 if not os.path.isdir(OutDir):
1242 # Get the group list and convert any named groups to numerics
1244 for groupname in AllowedGroupsPreload.strip().split(" "):
1245 GroupList[groupname] = True
1246 if 'allowedGroups' in host[1]:
1247 for groupname in host[1]['allowedGroups']:
1248 GroupList[groupname] = True
1249 for groupname in GroupList.keys():
1250 if groupname in GroupIDMap:
1251 GroupList[str(GroupIDMap[groupname])] = True
1254 if 'exportOptions' in host[1]:
1255 for extra in host[1]['exportOptions']:
1256 ExtraList[extra.upper()] = True
1259 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1261 DoLink(global_dir, OutDir, "debianhosts")
1262 DoLink(global_dir, OutDir, "ssh_known_hosts")
1263 DoLink(global_dir, OutDir, "disabled-accounts")
1266 if 'NOPASSWD' in ExtraList:
1267 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1269 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1271 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1272 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1274 # Now we know who we're allowing on the machine, export
1275 # the relevant ssh keys
1276 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1278 if not 'NOPASSWD' in ExtraList:
1279 GenShadow(accounts, OutDir + "shadow")
1281 # Link in global things
1282 if not 'NOMARKERS' in ExtraList:
1283 DoLink(global_dir, OutDir, "markers")
1284 DoLink(global_dir, OutDir, "mail-forward.cdb")
1285 DoLink(global_dir, OutDir, "mail-forward.db")
1286 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1287 DoLink(global_dir, OutDir, "mail-contentinspectionaction.db")
1288 DoLink(global_dir, OutDir, "mail-disable")
1289 DoLink(global_dir, OutDir, "mail-greylist")
1290 DoLink(global_dir, OutDir, "mail-callout")
1291 DoLink(global_dir, OutDir, "mail-rbl")
1292 DoLink(global_dir, OutDir, "mail-rhsbl")
1293 DoLink(global_dir, OutDir, "mail-whitelist")
1294 DoLink(global_dir, OutDir, "all-accounts.json")
1295 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1296 GenDBM(accounts, OutDir + "user-forward.db", 'emailForward')
1297 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1298 GenDBM(accounts, OutDir + "batv-tokens.db", 'bATVToken')
1299 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1300 GenDBM(accounts, OutDir + "default-mail-options.db", 'mailDefaultOptions')
1303 DoLink(global_dir, OutDir, "forward-alias")
1305 if 'DNS' in ExtraList:
1306 DoLink(global_dir, OutDir, "dns-zone")
1307 DoLink(global_dir, OutDir, "dns-sshfp")
1309 if 'AUTHKEYS' in ExtraList:
1310 DoLink(global_dir, OutDir, "authorized_keys")
1312 if 'BSMTP' in ExtraList:
1313 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1315 if 'PRIVATE' in ExtraList:
1316 DoLink(global_dir, OutDir, "debian-private")
1318 if 'GITOLITE' in ExtraList:
1319 DoLink(global_dir, OutDir, "ssh-gitolite")
1320 if 'exportOptions' in host[1]:
1321 for entry in host[1]['exportOptions']:
1322 v = entry.split('=',1)
1323 if v[0] != 'GITOLITE' or len(v) != 2: continue
1324 gitolite_accounts = filter(lambda x: IsInGroup(x, [v[1]], current_host), all_accounts)
1325 gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1326 GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(v[1],))
1328 if 'WEB-PASSWORDS' in ExtraList:
1329 DoLink(global_dir, OutDir, "web-passwords")
1331 if 'VOIP-PASSWORDS' in ExtraList:
1332 DoLink(global_dir, OutDir, "voip-passwords")
1334 if 'KEYRING' in ExtraList:
1336 bn = os.path.basename(k)
1337 if os.path.isdir(k):
1338 src = os.path.join(global_dir, bn)
1339 replaceTree(src, OutDir)
1341 DoLink(global_dir, OutDir, bn)
1345 bn = os.path.basename(k)
1346 target = os.path.join(OutDir, bn)
1347 if os.path.isdir(target):
1350 posix.remove(target)
1353 DoLink(global_dir, OutDir, "last_update.trace")
1356 def getLastLDAPChangeTime(l):
1357 mods = l.search_s('cn=log',
1358 ldap.SCOPE_ONELEVEL,
1359 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1364 # Sort the list by reqEnd
1365 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1366 # Take the last element in the array
1367 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1371 def getLastKeyringChangeTime():
1374 mt = os.path.getmtime(k)
1380 def getLastBuildTime(gdir):
1381 cache_last_ldap_mod = 0
1382 cache_last_unix_mod = 0
1386 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1387 cache_last_mod=fd.read().split()
1389 cache_last_ldap_mod = cache_last_mod[0]
1390 cache_last_unix_mod = int(cache_last_mod[1])
1391 cache_last_run = int(cache_last_mod[2])
1392 except IndexError, ValueError:
1396 if e.errno == errno.ENOENT:
1401 return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1404 parser = optparse.OptionParser()
1405 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1406 help="Output directory.")
1407 parser.add_option("-f", "--force", dest="force", action="store_true",
1408 help="Force generation, even if no update to LDAP has happened.")
1410 (options, args) = parser.parse_args()
1415 if options.generatedir is not None:
1416 generate_dir = os.environ['UD_GENERATEDIR']
1417 elif 'UD_GENERATEDIR' in os.environ:
1418 generate_dir = os.environ['UD_GENERATEDIR']
1420 generate_dir = GenerateDir
1423 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1424 lock = get_lock( lockf )
1426 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1429 l = make_ldap_conn()
1431 time_started = int(time.time())
1432 ldap_last_mod = getLastLDAPChangeTime(l)
1433 unix_last_mod = getLastKeyringChangeTime()
1434 cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1436 need_update = (ldap_last_mod > cache_last_ldap_mod) or (unix_last_mod > cache_last_unix_mod) or (time_started - last_run > MAX_UD_AGE)
1438 if not options.force and not need_update:
1439 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1440 fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1444 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1445 generate_all(generate_dir, l)
1446 tracefd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, time_started))
1450 if __name__ == "__main__":
1451 if 'UD_PROFILE' in os.environ:
1454 cProfile.run('ud_generate()', "udg_prof")
1455 p = pstats.Stats('udg_prof')
1456 ##p.sort_stats('time').print_stats()
1457 p.sort_stats('cumulative').print_stats()
1463 # vim:set shiftwidth=3: