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):
651 OldMask = os.umask(0022)
652 # nothing else does the fsync stuff, so why do it here?
653 fn = os.path.join(prefix,File).encode('ascii', 'ignore')
654 fntmp = os.path.join(prefix,File + '.tmp').encode('ascii', 'ignore')
660 Fdb = dbm.open(fntmp, "c")
663 # Write out the email address for each user
665 if not key in a: continue
671 posix.rename(fntmp,fn)
672 # Oops, something unspeakable happened.
677 # Generate the anon XEarth marker file
678 def GenMarkers(accounts, File):
681 F = open(File + ".tmp", "w")
683 # Write out the position for each user
685 if not ('latitude' in a and 'longitude' in a): continue
687 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
688 Line = Sanitize(Line) + "\n"
693 # Oops, something unspeakable happened.
699 # Generate the debian-private subscription list
700 def GenPrivate(accounts, File):
703 F = open(File + ".tmp", "w")
705 # Write out the position for each user
707 if not a.is_active_user(): continue
708 if a.is_guest_account(): continue
709 if not 'privateSub' in a: continue
711 Line = "%s"%(a['privateSub'])
712 Line = Sanitize(Line) + "\n"
717 # Oops, something unspeakable happened.
723 # Generate a list of locked accounts
724 def GenDisabledAccounts(accounts, File):
727 F = open(File + ".tmp", "w")
728 disabled_accounts = []
730 # Fetch all the users
732 if a.pw_active(): continue
733 Line = "%s:%s" % (a['uid'], "Account is locked")
734 disabled_accounts.append(a)
735 F.write(Sanitize(Line) + "\n")
737 # Oops, something unspeakable happened.
742 return disabled_accounts
744 # Generate the list of local addresses that refuse all mail
745 def GenMailDisable(accounts, File):
748 F = open(File + ".tmp", "w")
751 if not 'mailDisableMessage' in a: continue
752 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
753 Line = Sanitize(Line) + "\n"
756 # Oops, something unspeakable happened.
762 # Generate a list of uids that should have boolean affects applied
763 def GenMailBool(accounts, File, key):
766 F = open(File + ".tmp", "w")
769 if not key in a: continue
770 if not a[key] == 'TRUE': continue
771 Line = "%s"%(a['uid'])
772 Line = Sanitize(Line) + "\n"
775 # Oops, something unspeakable happened.
781 # Generate a list of hosts for RBL or whitelist purposes.
782 def GenMailList(accounts, File, key):
785 F = open(File + ".tmp", "w")
787 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
788 else: validregex = re.compile('^[-\w.]+$')
791 if not key in a: continue
793 filtered = filter(lambda z: validregex.match(z), a[key])
794 if len(filtered) == 0: continue
795 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
796 line = a['uid'] + ': ' + ' : '.join(filtered)
797 line = Sanitize(line) + "\n"
800 # Oops, something unspeakable happened.
806 def isRoleAccount(account):
807 return 'debianRoleAccount' in account['objectClass']
809 # Generate the DNS Zone file
810 def GenDNS(accounts, File):
813 F = open(File + ".tmp", "w")
815 # Fetch all the users
818 # Write out the zone file entry for each user
820 if not 'dnsZoneEntry' in a: continue
821 if not a.is_active_user() and not isRoleAccount(a): continue
822 if a.is_guest_account(): continue
825 F.write("; %s\n"%(a.email_address()))
826 for z in a["dnsZoneEntry"]:
827 Split = z.lower().split()
828 if Split[1].lower() == 'in':
829 Line = " ".join(Split) + "\n"
832 Host = Split[0] + DNSZone
833 if BSMTPCheck.match(Line) != None:
834 F.write("; Has BSMTP\n")
836 # Write some identification information
837 if not RRs.has_key(Host):
838 if Split[2].lower() in ["a", "aaaa"]:
839 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
840 for y in a["keyFingerPrint"]:
841 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
845 Line = "; Err %s"%(str(Split))
850 F.write("; Errors:\n")
851 for line in str(e).split("\n"):
852 F.write("; %s\n"%(line))
855 # Oops, something unspeakable happened.
863 socket.inet_pton(socket.AF_INET6, i)
868 def ExtractDNSInfo(x):
872 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
875 if x[1].has_key("ipHostNumber"):
876 for I in x[1]["ipHostNumber"]:
878 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
880 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
884 if 'sshRSAHostKey' in x[1]:
885 for I in x[1]["sshRSAHostKey"]:
887 if Split[0] == 'ssh-rsa':
889 if Split[0] == 'ssh-dss':
891 if Algorithm == None:
893 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
894 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
896 if 'architecture' in x[1]:
897 Arch = GetAttr(x, "architecture")
899 if x[1].has_key("machine"):
900 Mach = " " + GetAttr(x, "machine")
901 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
903 if x[1].has_key("mXRecord"):
904 for I in x[1]["mXRecord"]:
906 for e in MX_remap[I]:
907 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
909 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
913 # Generate the DNS records
914 def GenZoneRecords(host_attrs, File):
917 F = open(File + ".tmp", "w")
919 # Fetch all the hosts
921 if x[1].has_key("hostname") == 0:
924 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
927 DNSInfo = ExtractDNSInfo(x)
931 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
934 Line = "\t\t\t%s" % (Line)
938 # this would write sshfp lines for services on machines
939 # but we can't yet, since some are cnames and we'll make
940 # an invalid zonefile
942 # for i in x[1].get("purpose", []):
943 # m = PurposeHostField.match(i)
946 # # we ignore [[*..]] entries
947 # if m.startswith('*'):
949 # if m.startswith('-'):
952 # if not m.endswith(HostDomain):
954 # if not m.endswith('.'):
956 # for Line in DNSInfo:
957 # if isSSHFP.match(Line):
958 # Line = "%s\t%s" % (m, Line)
959 # F.write(Line + "\n")
961 # Oops, something unspeakable happened.
967 # Generate the BSMTP file
968 def GenBSMTP(accounts, File, HomePrefix):
971 F = open(File + ".tmp", "w")
973 # Write out the zone file entry for each user
975 if not 'dnsZoneEntry' in a: continue
976 if not a.is_active_user(): continue
979 for z in a["dnsZoneEntry"]:
980 Split = z.lower().split()
981 if Split[1].lower() == 'in':
982 for y in range(0, len(Split)):
985 Line = " ".join(Split) + "\n"
987 Host = Split[0] + DNSZone
988 if BSMTPCheck.match(Line) != None:
989 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
990 a['uid'], HomePrefix, a['uid'], Host))
993 F.write("; Errors\n")
996 # Oops, something unspeakable happened.
1002 def HostToIP(Host, mapped=True):
1006 if Host[1].has_key("ipHostNumber"):
1007 for addr in Host[1]["ipHostNumber"]:
1008 IPAdresses.append(addr)
1009 if not is_ipv6_addr(addr) and mapped == "True":
1010 IPAdresses.append("::ffff:"+addr)
1014 # Generate the ssh known hosts file
1015 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
1018 OldMask = os.umask(0022)
1019 F = open(File + ".tmp", "w", 0644)
1022 for x in host_attrs:
1023 if x[1].has_key("hostname") == 0 or \
1024 x[1].has_key("sshRSAHostKey") == 0:
1026 Host = GetAttr(x, "hostname")
1027 HostNames = [ Host ]
1028 if Host.endswith(HostDomain):
1029 HostNames.append(Host[:-(len(HostDomain) + 1)])
1031 # in the purpose field [[host|some other text]] (where some other text is optional)
1032 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1033 # file. But so that we don't have to add everything we link we can add an asterisk
1034 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
1035 # http linking it we also support [[-hostname]] entries.
1036 for i in x[1].get("purpose", []):
1037 m = PurposeHostField.match(i)
1040 # we ignore [[*..]] entries
1041 if m.startswith('*'):
1043 if m.startswith('-'):
1047 if m.endswith(HostDomain):
1048 HostNames.append(m[:-(len(HostDomain) + 1)])
1050 for I in x[1]["sshRSAHostKey"]:
1051 if mode and mode == 'authorized_keys':
1053 if 'sshdistAuthKeysHost' in x[1]:
1054 hosts += x[1]['sshdistAuthKeysHost']
1055 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1056 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1057 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1059 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1060 Line = Sanitize(Line) + "\n"
1062 # Oops, something unspeakable happened.
1068 # Generate the debianhosts file (list of all IP addresses)
1069 def GenHosts(host_attrs, File):
1072 OldMask = os.umask(0022)
1073 F = open(File + ".tmp", "w", 0644)
1078 for x in host_attrs:
1080 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1083 if not 'ipHostNumber' in x[1]:
1086 addrs = x[1]["ipHostNumber"]
1088 if addr not in seen:
1090 addr = Sanitize(addr) + "\n"
1093 # Oops, something unspeakable happened.
1099 def replaceTree(src, dst_basedir):
1100 bn = os.path.basename(src)
1101 dst = os.path.join(dst_basedir, bn)
1103 shutil.copytree(src, dst)
1105 def GenKeyrings(OutDir):
1107 if os.path.isdir(k):
1108 replaceTree(k, OutDir)
1110 shutil.copy(k, OutDir)
1113 def get_accounts(ldap_conn):
1114 # Fetch all the users
1115 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1116 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1117 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1118 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1119 "shadowExpire", "emailForward", "latitude", "longitude",\
1120 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1121 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1122 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1123 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1124 "mailContentInspectionAction", "webPassword", "voipPassword"])
1126 if passwd_attrs is None:
1127 raise UDEmptyList, "No Users"
1128 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1129 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1133 def get_hosts(ldap_conn):
1134 # Fetch all the hosts
1135 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1136 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1137 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1139 if HostAttrs == None:
1140 raise UDEmptyList, "No Hosts"
1142 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1147 def make_ldap_conn():
1148 # Connect to the ldap server
1150 # for testing purposes it's sometimes useful to pass username/password
1151 # via the environment
1152 if 'UD_CREDENTIALS' in os.environ:
1153 Pass = os.environ['UD_CREDENTIALS'].split()
1155 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1156 Pass = F.readline().strip().split(" ")
1158 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1164 def setup_group_maps(l):
1165 # Fetch all the groups
1168 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1169 ["gid", "gidNumber", "subGroup"])
1171 # Generate the subgroup_map and group_id_map
1173 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1175 if x[1].has_key("gidNumber") == 0:
1177 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1178 if x[1].has_key("subGroup") != 0:
1179 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1183 SubGroupMap = subgroup_map
1184 GroupIDMap = group_id_map
1186 def generate_all(global_dir, ldap_conn):
1187 accounts = get_accounts(ldap_conn)
1188 host_attrs = get_hosts(ldap_conn)
1191 # Generate global things
1192 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1194 accounts = filter(lambda x: not IsRetired(x), accounts)
1195 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1197 CheckForward(accounts)
1199 GenMailDisable(accounts, global_dir + "mail-disable")
1200 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1201 GenDBM(accounts, global_dir + "mail-forward.dbm", 'emailForward')
1202 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1203 GenDBM(accounts, global_dir + "mail-contentinspectionaction.dbm", 'mailContentInspectionAction')
1204 GenPrivate(accounts, global_dir + "debian-private")
1205 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1206 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1207 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1208 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1209 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1210 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1211 GenWebPassword(accounts, global_dir + "web-passwords")
1212 GenVoipPassword(accounts, global_dir + "voip-passwords")
1213 GenKeyrings(global_dir)
1216 GenForward(accounts, global_dir + "forward-alias")
1218 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1219 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1221 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1222 GenMarkers(accounts, global_dir + "markers")
1223 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1224 GenHosts(host_attrs, global_dir + "debianhosts")
1225 GenSSHGitolite(accounts, host_attrs, global_dir + "ssh-gitolite")
1227 GenDNS(accounts, global_dir + "dns-zone")
1228 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1230 setup_group_maps(ldap_conn)
1232 for host in host_attrs:
1233 if not "hostname" in host[1]:
1235 generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1237 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1238 current_host = host[1]['hostname'][0]
1239 OutDir = global_dir + current_host + '/'
1240 if not os.path.isdir(OutDir):
1243 # Get the group list and convert any named groups to numerics
1245 for groupname in AllowedGroupsPreload.strip().split(" "):
1246 GroupList[groupname] = True
1247 if 'allowedGroups' in host[1]:
1248 for groupname in host[1]['allowedGroups']:
1249 GroupList[groupname] = True
1250 for groupname in GroupList.keys():
1251 if groupname in GroupIDMap:
1252 GroupList[str(GroupIDMap[groupname])] = True
1255 if 'exportOptions' in host[1]:
1256 for extra in host[1]['exportOptions']:
1257 ExtraList[extra.upper()] = True
1260 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1262 DoLink(global_dir, OutDir, "debianhosts")
1263 DoLink(global_dir, OutDir, "ssh_known_hosts")
1264 DoLink(global_dir, OutDir, "disabled-accounts")
1267 if 'NOPASSWD' in ExtraList:
1268 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1270 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1272 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1273 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1275 # Now we know who we're allowing on the machine, export
1276 # the relevant ssh keys
1277 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1279 if not 'NOPASSWD' in ExtraList:
1280 GenShadow(accounts, OutDir + "shadow")
1282 # Link in global things
1283 if not 'NOMARKERS' in ExtraList:
1284 DoLink(global_dir, OutDir, "markers")
1285 DoLink(global_dir, OutDir, "mail-forward.cdb")
1286 DoLink(global_dir, OutDir, "mail-forward.dbm")
1287 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1288 DoLink(global_dir, OutDir, "mail-contentinspectionaction.dbm")
1289 DoLink(global_dir, OutDir, "mail-disable")
1290 DoLink(global_dir, OutDir, "mail-greylist")
1291 DoLink(global_dir, OutDir, "mail-callout")
1292 DoLink(global_dir, OutDir, "mail-rbl")
1293 DoLink(global_dir, OutDir, "mail-rhsbl")
1294 DoLink(global_dir, OutDir, "mail-whitelist")
1295 DoLink(global_dir, OutDir, "all-accounts.json")
1296 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1297 GenDBM(accounts, OutDir + "user-forward.dbm", 'emailForward')
1298 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1299 GenDBM(accounts, OutDir + "batv-tokens.dbm", 'bATVToken')
1300 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1301 GenDBM(accounts, OutDir + "default-mail-options.dbm", 'mailDefaultOptions')
1304 DoLink(global_dir, OutDir, "forward-alias")
1306 if 'DNS' in ExtraList:
1307 DoLink(global_dir, OutDir, "dns-zone")
1308 DoLink(global_dir, OutDir, "dns-sshfp")
1310 if 'AUTHKEYS' in ExtraList:
1311 DoLink(global_dir, OutDir, "authorized_keys")
1313 if 'BSMTP' in ExtraList:
1314 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1316 if 'PRIVATE' in ExtraList:
1317 DoLink(global_dir, OutDir, "debian-private")
1319 if 'GITOLITE' in ExtraList:
1320 DoLink(global_dir, OutDir, "ssh-gitolite")
1321 if 'exportOptions' in host[1]:
1322 for entry in host[1]['exportOptions']:
1323 v = entry.split('=',1)
1324 if v[0] != 'GITOLITE' or len(v) != 2: continue
1325 gitolite_accounts = filter(lambda x: IsInGroup(x, [v[1]], current_host), all_accounts)
1326 gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1327 GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(v[1],))
1329 if 'WEB-PASSWORDS' in ExtraList:
1330 DoLink(global_dir, OutDir, "web-passwords")
1332 if 'VOIP-PASSWORDS' in ExtraList:
1333 DoLink(global_dir, OutDir, "voip-passwords")
1335 if 'KEYRING' in ExtraList:
1337 bn = os.path.basename(k)
1338 if os.path.isdir(k):
1339 src = os.path.join(global_dir, bn)
1340 replaceTree(src, OutDir)
1342 DoLink(global_dir, OutDir, bn)
1346 bn = os.path.basename(k)
1347 target = os.path.join(OutDir, bn)
1348 if os.path.isdir(target):
1351 posix.remove(target)
1354 DoLink(global_dir, OutDir, "last_update.trace")
1357 def getLastLDAPChangeTime(l):
1358 mods = l.search_s('cn=log',
1359 ldap.SCOPE_ONELEVEL,
1360 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1365 # Sort the list by reqEnd
1366 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1367 # Take the last element in the array
1368 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1372 def getLastKeyringChangeTime():
1375 mt = os.path.getmtime(k)
1381 def getLastBuildTime(gdir):
1382 cache_last_ldap_mod = 0
1383 cache_last_unix_mod = 0
1387 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1388 cache_last_mod=fd.read().split()
1390 cache_last_ldap_mod = cache_last_mod[0]
1391 cache_last_unix_mod = int(cache_last_mod[1])
1392 cache_last_run = int(cache_last_mod[2])
1393 except IndexError, ValueError:
1397 if e.errno == errno.ENOENT:
1402 return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1405 parser = optparse.OptionParser()
1406 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1407 help="Output directory.")
1408 parser.add_option("-f", "--force", dest="force", action="store_true",
1409 help="Force generation, even if no update to LDAP has happened.")
1411 (options, args) = parser.parse_args()
1416 if options.generatedir is not None:
1417 generate_dir = os.environ['UD_GENERATEDIR']
1418 elif 'UD_GENERATEDIR' in os.environ:
1419 generate_dir = os.environ['UD_GENERATEDIR']
1421 generate_dir = GenerateDir
1424 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1425 lock = get_lock( lockf )
1427 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1430 l = make_ldap_conn()
1432 time_started = int(time.time())
1433 ldap_last_mod = getLastLDAPChangeTime(l)
1434 unix_last_mod = getLastKeyringChangeTime()
1435 cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1437 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)
1439 if not options.force and not need_update:
1440 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1441 fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1445 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1446 generate_all(generate_dir, l)
1447 tracefd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, time_started))
1451 if __name__ == "__main__":
1452 if 'UD_PROFILE' in os.environ:
1455 cProfile.run('ud_generate()', "udg_prof")
1456 p = pstats.Stats('udg_prof')
1457 ##p.sort_stats('time').print_stats()
1458 p.sort_stats('cumulative').print_stats()
1464 # vim:set shiftwidth=3: