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 + ".tmp", "c")
661 # Write out the email address for each user
663 if not key in a: continue
674 # Generate the anon XEarth marker file
675 def GenMarkers(accounts, File):
678 F = open(File + ".tmp", "w")
680 # Write out the position for each user
682 if not ('latitude' in a and 'longitude' in a): continue
684 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
685 Line = Sanitize(Line) + "\n"
690 # Oops, something unspeakable happened.
696 # Generate the debian-private subscription list
697 def GenPrivate(accounts, File):
700 F = open(File + ".tmp", "w")
702 # Write out the position for each user
704 if not a.is_active_user(): continue
705 if a.is_guest_account(): continue
706 if not 'privateSub' in a: continue
708 Line = "%s"%(a['privateSub'])
709 Line = Sanitize(Line) + "\n"
714 # Oops, something unspeakable happened.
720 # Generate a list of locked accounts
721 def GenDisabledAccounts(accounts, File):
724 F = open(File + ".tmp", "w")
725 disabled_accounts = []
727 # Fetch all the users
729 if a.pw_active(): continue
730 Line = "%s:%s" % (a['uid'], "Account is locked")
731 disabled_accounts.append(a)
732 F.write(Sanitize(Line) + "\n")
734 # Oops, something unspeakable happened.
739 return disabled_accounts
741 # Generate the list of local addresses that refuse all mail
742 def GenMailDisable(accounts, File):
745 F = open(File + ".tmp", "w")
748 if not 'mailDisableMessage' in a: continue
749 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
750 Line = Sanitize(Line) + "\n"
753 # Oops, something unspeakable happened.
759 # Generate a list of uids that should have boolean affects applied
760 def GenMailBool(accounts, File, key):
763 F = open(File + ".tmp", "w")
766 if not key in a: continue
767 if not a[key] == 'TRUE': continue
768 Line = "%s"%(a['uid'])
769 Line = Sanitize(Line) + "\n"
772 # Oops, something unspeakable happened.
778 # Generate a list of hosts for RBL or whitelist purposes.
779 def GenMailList(accounts, File, key):
782 F = open(File + ".tmp", "w")
784 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
785 else: validregex = re.compile('^[-\w.]+$')
788 if not key in a: continue
790 filtered = filter(lambda z: validregex.match(z), a[key])
791 if len(filtered) == 0: continue
792 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
793 line = a['uid'] + ': ' + ' : '.join(filtered)
794 line = Sanitize(line) + "\n"
797 # Oops, something unspeakable happened.
803 def isRoleAccount(account):
804 return 'debianRoleAccount' in account['objectClass']
806 # Generate the DNS Zone file
807 def GenDNS(accounts, File):
810 F = open(File + ".tmp", "w")
812 # Fetch all the users
815 # Write out the zone file entry for each user
817 if not 'dnsZoneEntry' in a: continue
818 if not a.is_active_user() and not isRoleAccount(a): continue
819 if a.is_guest_account(): continue
822 F.write("; %s\n"%(a.email_address()))
823 for z in a["dnsZoneEntry"]:
824 Split = z.lower().split()
825 if Split[1].lower() == 'in':
826 Line = " ".join(Split) + "\n"
829 Host = Split[0] + DNSZone
830 if BSMTPCheck.match(Line) != None:
831 F.write("; Has BSMTP\n")
833 # Write some identification information
834 if not RRs.has_key(Host):
835 if Split[2].lower() in ["a", "aaaa"]:
836 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
837 for y in a["keyFingerPrint"]:
838 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
842 Line = "; Err %s"%(str(Split))
847 F.write("; Errors:\n")
848 for line in str(e).split("\n"):
849 F.write("; %s\n"%(line))
852 # Oops, something unspeakable happened.
860 socket.inet_pton(socket.AF_INET6, i)
865 def ExtractDNSInfo(x):
869 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
872 if x[1].has_key("ipHostNumber"):
873 for I in x[1]["ipHostNumber"]:
875 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
877 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
881 if 'sshRSAHostKey' in x[1]:
882 for I in x[1]["sshRSAHostKey"]:
884 if Split[0] == 'ssh-rsa':
886 if Split[0] == 'ssh-dss':
888 if Algorithm == None:
890 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
891 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
893 if 'architecture' in x[1]:
894 Arch = GetAttr(x, "architecture")
896 if x[1].has_key("machine"):
897 Mach = " " + GetAttr(x, "machine")
898 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
900 if x[1].has_key("mXRecord"):
901 for I in x[1]["mXRecord"]:
903 for e in MX_remap[I]:
904 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
906 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
910 # Generate the DNS records
911 def GenZoneRecords(host_attrs, File):
914 F = open(File + ".tmp", "w")
916 # Fetch all the hosts
918 if x[1].has_key("hostname") == 0:
921 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
924 DNSInfo = ExtractDNSInfo(x)
928 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
931 Line = "\t\t\t%s" % (Line)
935 # this would write sshfp lines for services on machines
936 # but we can't yet, since some are cnames and we'll make
937 # an invalid zonefile
939 # for i in x[1].get("purpose", []):
940 # m = PurposeHostField.match(i)
943 # # we ignore [[*..]] entries
944 # if m.startswith('*'):
946 # if m.startswith('-'):
949 # if not m.endswith(HostDomain):
951 # if not m.endswith('.'):
953 # for Line in DNSInfo:
954 # if isSSHFP.match(Line):
955 # Line = "%s\t%s" % (m, Line)
956 # F.write(Line + "\n")
958 # Oops, something unspeakable happened.
964 # Generate the BSMTP file
965 def GenBSMTP(accounts, File, HomePrefix):
968 F = open(File + ".tmp", "w")
970 # Write out the zone file entry for each user
972 if not 'dnsZoneEntry' in a: continue
973 if not a.is_active_user(): continue
976 for z in a["dnsZoneEntry"]:
977 Split = z.lower().split()
978 if Split[1].lower() == 'in':
979 for y in range(0, len(Split)):
982 Line = " ".join(Split) + "\n"
984 Host = Split[0] + DNSZone
985 if BSMTPCheck.match(Line) != None:
986 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
987 a['uid'], HomePrefix, a['uid'], Host))
990 F.write("; Errors\n")
993 # Oops, something unspeakable happened.
999 def HostToIP(Host, mapped=True):
1003 if Host[1].has_key("ipHostNumber"):
1004 for addr in Host[1]["ipHostNumber"]:
1005 IPAdresses.append(addr)
1006 if not is_ipv6_addr(addr) and mapped == "True":
1007 IPAdresses.append("::ffff:"+addr)
1011 # Generate the ssh known hosts file
1012 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
1015 OldMask = os.umask(0022)
1016 F = open(File + ".tmp", "w", 0644)
1019 for x in host_attrs:
1020 if x[1].has_key("hostname") == 0 or \
1021 x[1].has_key("sshRSAHostKey") == 0:
1023 Host = GetAttr(x, "hostname")
1024 HostNames = [ Host ]
1025 if Host.endswith(HostDomain):
1026 HostNames.append(Host[:-(len(HostDomain) + 1)])
1028 # in the purpose field [[host|some other text]] (where some other text is optional)
1029 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1030 # file. But so that we don't have to add everything we link we can add an asterisk
1031 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
1032 # http linking it we also support [[-hostname]] entries.
1033 for i in x[1].get("purpose", []):
1034 m = PurposeHostField.match(i)
1037 # we ignore [[*..]] entries
1038 if m.startswith('*'):
1040 if m.startswith('-'):
1044 if m.endswith(HostDomain):
1045 HostNames.append(m[:-(len(HostDomain) + 1)])
1047 for I in x[1]["sshRSAHostKey"]:
1048 if mode and mode == 'authorized_keys':
1050 if 'sshdistAuthKeysHost' in x[1]:
1051 hosts += x[1]['sshdistAuthKeysHost']
1052 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1053 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1054 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1056 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1057 Line = Sanitize(Line) + "\n"
1059 # Oops, something unspeakable happened.
1065 # Generate the debianhosts file (list of all IP addresses)
1066 def GenHosts(host_attrs, File):
1069 OldMask = os.umask(0022)
1070 F = open(File + ".tmp", "w", 0644)
1075 for x in host_attrs:
1077 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1080 if not 'ipHostNumber' in x[1]:
1083 addrs = x[1]["ipHostNumber"]
1085 if addr not in seen:
1087 addr = Sanitize(addr) + "\n"
1090 # Oops, something unspeakable happened.
1096 def replaceTree(src, dst_basedir):
1097 bn = os.path.basename(src)
1098 dst = os.path.join(dst_basedir, bn)
1100 shutil.copytree(src, dst)
1102 def GenKeyrings(OutDir):
1104 if os.path.isdir(k):
1105 replaceTree(k, OutDir)
1107 shutil.copy(k, OutDir)
1110 def get_accounts(ldap_conn):
1111 # Fetch all the users
1112 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1113 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1114 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1115 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1116 "shadowExpire", "emailForward", "latitude", "longitude",\
1117 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1118 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1119 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1120 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1121 "mailContentInspectionAction", "webPassword", "voipPassword"])
1123 if passwd_attrs is None:
1124 raise UDEmptyList, "No Users"
1125 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1126 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1130 def get_hosts(ldap_conn):
1131 # Fetch all the hosts
1132 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1133 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1134 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1136 if HostAttrs == None:
1137 raise UDEmptyList, "No Hosts"
1139 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1144 def make_ldap_conn():
1145 # Connect to the ldap server
1147 # for testing purposes it's sometimes useful to pass username/password
1148 # via the environment
1149 if 'UD_CREDENTIALS' in os.environ:
1150 Pass = os.environ['UD_CREDENTIALS'].split()
1152 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1153 Pass = F.readline().strip().split(" ")
1155 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1161 def setup_group_maps(l):
1162 # Fetch all the groups
1165 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1166 ["gid", "gidNumber", "subGroup"])
1168 # Generate the subgroup_map and group_id_map
1170 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1172 if x[1].has_key("gidNumber") == 0:
1174 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1175 if x[1].has_key("subGroup") != 0:
1176 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1180 SubGroupMap = subgroup_map
1181 GroupIDMap = group_id_map
1183 def generate_all(global_dir, ldap_conn):
1184 accounts = get_accounts(ldap_conn)
1185 host_attrs = get_hosts(ldap_conn)
1188 # Generate global things
1189 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1191 accounts = filter(lambda x: not IsRetired(x), accounts)
1192 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1194 CheckForward(accounts)
1196 GenMailDisable(accounts, global_dir + "mail-disable")
1197 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1198 GenDBM(accounts, global_dir + "mail-forward", 'emailForward')
1199 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1200 GenDBM(accounts, global_dir + "mail-contentinspectionaction", 'mailContentInspectionAction')
1201 GenPrivate(accounts, global_dir + "debian-private")
1202 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1203 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1204 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1205 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1206 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1207 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1208 GenWebPassword(accounts, global_dir + "web-passwords")
1209 GenVoipPassword(accounts, global_dir + "voip-passwords")
1210 GenKeyrings(global_dir)
1213 GenForward(accounts, global_dir + "forward-alias")
1215 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1216 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1218 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1219 GenMarkers(accounts, global_dir + "markers")
1220 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1221 GenHosts(host_attrs, global_dir + "debianhosts")
1222 GenSSHGitolite(accounts, host_attrs, global_dir + "ssh-gitolite")
1224 GenDNS(accounts, global_dir + "dns-zone")
1225 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1227 setup_group_maps(ldap_conn)
1229 for host in host_attrs:
1230 if not "hostname" in host[1]:
1232 generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1234 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1235 current_host = host[1]['hostname'][0]
1236 OutDir = global_dir + current_host + '/'
1237 if not os.path.isdir(OutDir):
1240 # Get the group list and convert any named groups to numerics
1242 for groupname in AllowedGroupsPreload.strip().split(" "):
1243 GroupList[groupname] = True
1244 if 'allowedGroups' in host[1]:
1245 for groupname in host[1]['allowedGroups']:
1246 GroupList[groupname] = True
1247 for groupname in GroupList.keys():
1248 if groupname in GroupIDMap:
1249 GroupList[str(GroupIDMap[groupname])] = True
1252 if 'exportOptions' in host[1]:
1253 for extra in host[1]['exportOptions']:
1254 ExtraList[extra.upper()] = True
1257 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1259 DoLink(global_dir, OutDir, "debianhosts")
1260 DoLink(global_dir, OutDir, "ssh_known_hosts")
1261 DoLink(global_dir, OutDir, "disabled-accounts")
1264 if 'NOPASSWD' in ExtraList:
1265 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1267 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1269 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1270 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1272 # Now we know who we're allowing on the machine, export
1273 # the relevant ssh keys
1274 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1276 if not 'NOPASSWD' in ExtraList:
1277 GenShadow(accounts, OutDir + "shadow")
1279 # Link in global things
1280 if not 'NOMARKERS' in ExtraList:
1281 DoLink(global_dir, OutDir, "markers")
1282 DoLink(global_dir, OutDir, "mail-forward.cdb")
1283 DoLink(global_dir, OutDir, "mail-forward.db")
1284 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1285 DoLink(global_dir, OutDir, "mail-contentinspectionaction.db")
1286 DoLink(global_dir, OutDir, "mail-disable")
1287 DoLink(global_dir, OutDir, "mail-greylist")
1288 DoLink(global_dir, OutDir, "mail-callout")
1289 DoLink(global_dir, OutDir, "mail-rbl")
1290 DoLink(global_dir, OutDir, "mail-rhsbl")
1291 DoLink(global_dir, OutDir, "mail-whitelist")
1292 DoLink(global_dir, OutDir, "all-accounts.json")
1293 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1294 GenDBM(accounts, OutDir + "user-forward", 'emailForward')
1295 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1296 GenDBM(accounts, OutDir + "batv-tokens", 'bATVToken')
1297 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1298 GenDBM(accounts, OutDir + "default-mail-options", 'mailDefaultOptions')
1301 DoLink(global_dir, OutDir, "forward-alias")
1303 if 'DNS' in ExtraList:
1304 DoLink(global_dir, OutDir, "dns-zone")
1305 DoLink(global_dir, OutDir, "dns-sshfp")
1307 if 'AUTHKEYS' in ExtraList:
1308 DoLink(global_dir, OutDir, "authorized_keys")
1310 if 'BSMTP' in ExtraList:
1311 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1313 if 'PRIVATE' in ExtraList:
1314 DoLink(global_dir, OutDir, "debian-private")
1316 if 'GITOLITE' in ExtraList:
1317 DoLink(global_dir, OutDir, "ssh-gitolite")
1318 if 'exportOptions' in host[1]:
1319 for entry in host[1]['exportOptions']:
1320 v = entry.split('=',1)
1321 if v[0] != 'GITOLITE' or len(v) != 2: continue
1322 gitolite_accounts = filter(lambda x: IsInGroup(x, [v[1]], current_host), all_accounts)
1323 gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1324 GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(v[1],))
1326 if 'WEB-PASSWORDS' in ExtraList:
1327 DoLink(global_dir, OutDir, "web-passwords")
1329 if 'VOIP-PASSWORDS' in ExtraList:
1330 DoLink(global_dir, OutDir, "voip-passwords")
1332 if 'KEYRING' in ExtraList:
1334 bn = os.path.basename(k)
1335 if os.path.isdir(k):
1336 src = os.path.join(global_dir, bn)
1337 replaceTree(src, OutDir)
1339 DoLink(global_dir, OutDir, bn)
1343 bn = os.path.basename(k)
1344 target = os.path.join(OutDir, bn)
1345 if os.path.isdir(target):
1348 posix.remove(target)
1351 DoLink(global_dir, OutDir, "last_update.trace")
1354 def getLastLDAPChangeTime(l):
1355 mods = l.search_s('cn=log',
1356 ldap.SCOPE_ONELEVEL,
1357 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1362 # Sort the list by reqEnd
1363 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1364 # Take the last element in the array
1365 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1369 def getLastKeyringChangeTime():
1372 mt = os.path.getmtime(k)
1378 def getLastBuildTime(gdir):
1379 cache_last_ldap_mod = 0
1380 cache_last_unix_mod = 0
1384 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1385 cache_last_mod=fd.read().split()
1387 cache_last_ldap_mod = cache_last_mod[0]
1388 cache_last_unix_mod = int(cache_last_mod[1])
1389 cache_last_run = int(cache_last_mod[2])
1390 except IndexError, ValueError:
1394 if e.errno == errno.ENOENT:
1399 return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1402 parser = optparse.OptionParser()
1403 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1404 help="Output directory.")
1405 parser.add_option("-f", "--force", dest="force", action="store_true",
1406 help="Force generation, even if no update to LDAP has happened.")
1408 (options, args) = parser.parse_args()
1413 if options.generatedir is not None:
1414 generate_dir = os.environ['UD_GENERATEDIR']
1415 elif 'UD_GENERATEDIR' in os.environ:
1416 generate_dir = os.environ['UD_GENERATEDIR']
1418 generate_dir = GenerateDir
1421 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1422 lock = get_lock( lockf )
1424 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1427 l = make_ldap_conn()
1429 time_started = int(time.time())
1430 ldap_last_mod = getLastLDAPChangeTime(l)
1431 unix_last_mod = getLastKeyringChangeTime()
1432 cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1434 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)
1436 if not options.force and not need_update:
1437 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1438 fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1442 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1443 generate_all(generate_dir, l)
1444 tracefd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, time_started))
1448 if __name__ == "__main__":
1449 if 'UD_PROFILE' in os.environ:
1452 cProfile.run('ud_generate()', "udg_prof")
1453 p = pstats.Stats('udg_prof')
1454 ##p.sort_stats('time').print_stats()
1455 p.sort_stats('cumulative').print_stats()
1461 # vim:set shiftwidth=3: