3 # Generates passwd, shadow and group files from the ldap directory.
5 # Copyright (c) 2000-2001 Jason Gunthorpe <jgg@debian.org>
6 # Copyright (c) 2003-2004 James Troup <troup@debian.org>
7 # Copyright (c) 2004-2005,7 Joey Schulze <joey@infodrom.org>
8 # Copyright (c) 2001-2007 Ryan Murray <rmurray@debian.org>
9 # Copyright (c) 2008,2009,2010,2011 Peter Palfrader <peter@palfrader.org>
10 # Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
11 # Copyright (c) 2008 Mark Hymers <mhy@debian.org>
12 # Copyright (c) 2008 Luk Claes <luk@debian.org>
13 # Copyright (c) 2008 Thomas Viehmann <tv@beamnet.de>
14 # Copyright (c) 2009 Stephen Gran <steve@lobefin.net>
15 # Copyright (c) 2010 Helmut Grohne <helmut@subdivi.de>
17 # This program is free software; you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation; either version 2 of the License, or
20 # (at your option) any later version.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
31 import string, re, time, ldap, optparse, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp, fcntl
32 from userdir_ldap import *
33 from userdir_exceptions import *
35 from xml.etree.ElementTree import Element, SubElement, Comment
36 from xml.etree import ElementTree
37 from xml.dom import minidom
39 from cStringIO import StringIO
41 from StringIO import StringIO
43 import simplejson as json
46 if not '__author__' in json.__dict__:
47 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
48 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
51 sys.stderr.write("You should probably not run ud-generate as root.\n")
63 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
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 # Generate the anon XEarth marker file
649 def GenMarkers(accounts, File):
652 F = open(File + ".tmp", "w")
654 # Write out the position for each user
656 if not ('latitude' in a and 'longitude' in a): continue
658 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
659 Line = Sanitize(Line) + "\n"
664 # Oops, something unspeakable happened.
670 # Generate the debian-private subscription list
671 def GenPrivate(accounts, File):
674 F = open(File + ".tmp", "w")
676 # Write out the position for each user
678 if not a.is_active_user(): continue
679 if a.is_guest_account(): continue
680 if not 'privateSub' in a: continue
682 Line = "%s"%(a['privateSub'])
683 Line = Sanitize(Line) + "\n"
688 # Oops, something unspeakable happened.
694 # Generate a list of locked accounts
695 def GenDisabledAccounts(accounts, File):
698 F = open(File + ".tmp", "w")
699 disabled_accounts = []
701 # Fetch all the users
703 if a.pw_active(): continue
704 Line = "%s:%s" % (a['uid'], "Account is locked")
705 disabled_accounts.append(a)
706 F.write(Sanitize(Line) + "\n")
708 # Oops, something unspeakable happened.
713 return disabled_accounts
715 # Generate the list of local addresses that refuse all mail
716 def GenMailDisable(accounts, File):
719 F = open(File + ".tmp", "w")
722 if not 'mailDisableMessage' in a: continue
723 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
724 Line = Sanitize(Line) + "\n"
727 # Oops, something unspeakable happened.
733 # Generate a list of uids that should have boolean affects applied
734 def GenMailBool(accounts, File, key):
737 F = open(File + ".tmp", "w")
740 if not key in a: continue
741 if not a[key] == 'TRUE': continue
742 Line = "%s"%(a['uid'])
743 Line = Sanitize(Line) + "\n"
746 # Oops, something unspeakable happened.
752 # Generate a list of hosts for RBL or whitelist purposes.
753 def GenMailList(accounts, File, key):
756 F = open(File + ".tmp", "w")
758 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
759 else: validregex = re.compile('^[-\w.]+$')
762 if not key in a: continue
764 filtered = filter(lambda z: validregex.match(z), a[key])
765 if len(filtered) == 0: continue
766 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
767 line = a['uid'] + ': ' + ' : '.join(filtered)
768 line = Sanitize(line) + "\n"
771 # Oops, something unspeakable happened.
777 def isRoleAccount(account):
778 return 'debianRoleAccount' in account['objectClass']
780 # Generate the DNS Zone file
781 def GenDNS(accounts, File):
784 F = open(File + ".tmp", "w")
786 # Fetch all the users
789 # Write out the zone file entry for each user
791 if not 'dnsZoneEntry' in a: continue
792 if not a.is_active_user() and not isRoleAccount(a): continue
793 if a.is_guest_account(): continue
796 F.write("; %s\n"%(a.email_address()))
797 for z in a["dnsZoneEntry"]:
798 Split = z.lower().split()
799 if Split[1].lower() == 'in':
800 Line = " ".join(Split) + "\n"
803 Host = Split[0] + DNSZone
804 if BSMTPCheck.match(Line) != None:
805 F.write("; Has BSMTP\n")
807 # Write some identification information
808 if not RRs.has_key(Host):
809 if Split[2].lower() in ["a", "aaaa"]:
810 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
811 for y in a["keyFingerPrint"]:
812 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
816 Line = "; Err %s"%(str(Split))
821 F.write("; Errors:\n")
822 for line in str(e).split("\n"):
823 F.write("; %s\n"%(line))
826 # Oops, something unspeakable happened.
834 socket.inet_pton(socket.AF_INET6, i)
839 def ExtractDNSInfo(x):
843 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
846 if x[1].has_key("ipHostNumber"):
847 for I in x[1]["ipHostNumber"]:
849 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
851 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
855 if 'sshRSAHostKey' in x[1]:
856 for I in x[1]["sshRSAHostKey"]:
858 if Split[0] == 'ssh-rsa':
860 if Split[0] == 'ssh-dss':
862 if Algorithm == None:
864 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
865 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
867 if 'architecture' in x[1]:
868 Arch = GetAttr(x, "architecture")
870 if x[1].has_key("machine"):
871 Mach = " " + GetAttr(x, "machine")
872 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
874 if x[1].has_key("mXRecord"):
875 for I in x[1]["mXRecord"]:
877 for e in MX_remap[I]:
878 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
880 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
884 # Generate the DNS records
885 def GenZoneRecords(host_attrs, File):
888 F = open(File + ".tmp", "w")
890 # Fetch all the hosts
892 if x[1].has_key("hostname") == 0:
895 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
898 DNSInfo = ExtractDNSInfo(x)
902 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
905 Line = "\t\t\t%s" % (Line)
909 # this would write sshfp lines for services on machines
910 # but we can't yet, since some are cnames and we'll make
911 # an invalid zonefile
913 # for i in x[1].get("purpose", []):
914 # m = PurposeHostField.match(i)
917 # # we ignore [[*..]] entries
918 # if m.startswith('*'):
920 # if m.startswith('-'):
923 # if not m.endswith(HostDomain):
925 # if not m.endswith('.'):
927 # for Line in DNSInfo:
928 # if isSSHFP.match(Line):
929 # Line = "%s\t%s" % (m, Line)
930 # F.write(Line + "\n")
932 # Oops, something unspeakable happened.
938 # Generate the BSMTP file
939 def GenBSMTP(accounts, File, HomePrefix):
942 F = open(File + ".tmp", "w")
944 # Write out the zone file entry for each user
946 if not 'dnsZoneEntry' in a: continue
947 if not a.is_active_user(): continue
950 for z in a["dnsZoneEntry"]:
951 Split = z.lower().split()
952 if Split[1].lower() == 'in':
953 for y in range(0, len(Split)):
956 Line = " ".join(Split) + "\n"
958 Host = Split[0] + DNSZone
959 if BSMTPCheck.match(Line) != None:
960 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
961 a['uid'], HomePrefix, a['uid'], Host))
964 F.write("; Errors\n")
967 # Oops, something unspeakable happened.
973 def HostToIP(Host, mapped=True):
977 if Host[1].has_key("ipHostNumber"):
978 for addr in Host[1]["ipHostNumber"]:
979 IPAdresses.append(addr)
980 if not is_ipv6_addr(addr) and mapped == "True":
981 IPAdresses.append("::ffff:"+addr)
985 # Generate the ssh known hosts file
986 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
989 OldMask = os.umask(0022)
990 F = open(File + ".tmp", "w", 0644)
994 if x[1].has_key("hostname") == 0 or \
995 x[1].has_key("sshRSAHostKey") == 0:
997 Host = GetAttr(x, "hostname")
999 if Host.endswith(HostDomain):
1000 HostNames.append(Host[:-(len(HostDomain) + 1)])
1002 # in the purpose field [[host|some other text]] (where some other text is optional)
1003 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1004 # file. But so that we don't have to add everything we link we can add an asterisk
1005 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
1006 # http linking it we also support [[-hostname]] entries.
1007 for i in x[1].get("purpose", []):
1008 m = PurposeHostField.match(i)
1011 # we ignore [[*..]] entries
1012 if m.startswith('*'):
1014 if m.startswith('-'):
1018 if m.endswith(HostDomain):
1019 HostNames.append(m[:-(len(HostDomain) + 1)])
1021 for I in x[1]["sshRSAHostKey"]:
1022 if mode and mode == 'authorized_keys':
1024 if 'sshdistAuthKeysHost' in x[1]:
1025 hosts += x[1]['sshdistAuthKeysHost']
1026 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1027 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1028 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1030 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1031 Line = Sanitize(Line) + "\n"
1033 # Oops, something unspeakable happened.
1039 # Generate the debianhosts file (list of all IP addresses)
1040 def GenHosts(host_attrs, File):
1043 OldMask = os.umask(0022)
1044 F = open(File + ".tmp", "w", 0644)
1049 for x in host_attrs:
1051 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1054 if not 'ipHostNumber' in x[1]:
1057 addrs = x[1]["ipHostNumber"]
1059 if addr not in seen:
1061 addr = Sanitize(addr) + "\n"
1064 # Oops, something unspeakable happened.
1070 def replaceTree(src, dst_basedir):
1071 bn = os.path.basename(src)
1072 dst = os.path.join(dst_basedir, bn)
1074 shutil.copytree(src, dst)
1076 def GenKeyrings(OutDir):
1078 if os.path.isdir(k):
1079 replaceTree(k, OutDir)
1081 shutil.copy(k, OutDir)
1084 def get_accounts(ldap_conn):
1085 # Fetch all the users
1086 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1087 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1088 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1089 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1090 "shadowExpire", "emailForward", "latitude", "longitude",\
1091 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1092 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1093 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1094 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1095 "mailContentInspectionAction", "webPassword", "voipPassword"])
1097 if passwd_attrs is None:
1098 raise UDEmptyList, "No Users"
1099 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1100 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1104 def get_hosts(ldap_conn):
1105 # Fetch all the hosts
1106 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1107 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1108 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1110 if HostAttrs == None:
1111 raise UDEmptyList, "No Hosts"
1113 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1118 def make_ldap_conn():
1119 # Connect to the ldap server
1121 # for testing purposes it's sometimes useful to pass username/password
1122 # via the environment
1123 if 'UD_CREDENTIALS' in os.environ:
1124 Pass = os.environ['UD_CREDENTIALS'].split()
1126 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1127 Pass = F.readline().strip().split(" ")
1129 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1135 def setup_group_maps(l):
1136 # Fetch all the groups
1139 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1140 ["gid", "gidNumber", "subGroup"])
1142 # Generate the subgroup_map and group_id_map
1144 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1146 if x[1].has_key("gidNumber") == 0:
1148 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1149 if x[1].has_key("subGroup") != 0:
1150 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1154 SubGroupMap = subgroup_map
1155 GroupIDMap = group_id_map
1157 def generate_all(global_dir, ldap_conn):
1158 accounts = get_accounts(ldap_conn)
1159 host_attrs = get_hosts(ldap_conn)
1162 # Generate global things
1163 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1165 accounts = filter(lambda x: not IsRetired(x), accounts)
1166 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1168 CheckForward(accounts)
1170 GenMailDisable(accounts, global_dir + "mail-disable")
1171 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1172 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1173 GenPrivate(accounts, global_dir + "debian-private")
1174 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1175 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1176 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1177 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1178 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1179 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1180 GenWebPassword(accounts, global_dir + "web-passwords")
1181 GenVoipPassword(accounts, global_dir + "voip-passwords")
1182 GenKeyrings(global_dir)
1185 GenForward(accounts, global_dir + "forward-alias")
1187 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1188 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1190 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1191 GenMarkers(accounts, global_dir + "markers")
1192 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1193 GenHosts(host_attrs, global_dir + "debianhosts")
1194 GenSSHGitolite(accounts, host_attrs, global_dir + "ssh-gitolite")
1196 GenDNS(accounts, global_dir + "dns-zone")
1197 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1199 setup_group_maps(ldap_conn)
1201 for host in host_attrs:
1202 if not "hostname" in host[1]:
1204 generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1206 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1207 current_host = host[1]['hostname'][0]
1208 OutDir = global_dir + current_host + '/'
1209 if not os.path.isdir(OutDir):
1212 # Get the group list and convert any named groups to numerics
1214 for groupname in AllowedGroupsPreload.strip().split(" "):
1215 GroupList[groupname] = True
1216 if 'allowedGroups' in host[1]:
1217 for groupname in host[1]['allowedGroups']:
1218 GroupList[groupname] = True
1219 for groupname in GroupList.keys():
1220 if groupname in GroupIDMap:
1221 GroupList[str(GroupIDMap[groupname])] = True
1224 if 'exportOptions' in host[1]:
1225 for extra in host[1]['exportOptions']:
1226 ExtraList[extra.upper()] = True
1229 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1231 DoLink(global_dir, OutDir, "debianhosts")
1232 DoLink(global_dir, OutDir, "ssh_known_hosts")
1233 DoLink(global_dir, OutDir, "disabled-accounts")
1236 if 'NOPASSWD' in ExtraList:
1237 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1239 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1241 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1242 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1244 # Now we know who we're allowing on the machine, export
1245 # the relevant ssh keys
1246 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1248 if not 'NOPASSWD' in ExtraList:
1249 GenShadow(accounts, OutDir + "shadow")
1251 # Link in global things
1252 if not 'NOMARKERS' in ExtraList:
1253 DoLink(global_dir, OutDir, "markers")
1254 DoLink(global_dir, OutDir, "mail-forward.cdb")
1255 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1256 DoLink(global_dir, OutDir, "mail-disable")
1257 DoLink(global_dir, OutDir, "mail-greylist")
1258 DoLink(global_dir, OutDir, "mail-callout")
1259 DoLink(global_dir, OutDir, "mail-rbl")
1260 DoLink(global_dir, OutDir, "mail-rhsbl")
1261 DoLink(global_dir, OutDir, "mail-whitelist")
1262 DoLink(global_dir, OutDir, "all-accounts.json")
1263 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1264 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1265 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1268 DoLink(global_dir, OutDir, "forward-alias")
1270 if 'DNS' in ExtraList:
1271 DoLink(global_dir, OutDir, "dns-zone")
1272 DoLink(global_dir, OutDir, "dns-sshfp")
1274 if 'AUTHKEYS' in ExtraList:
1275 DoLink(global_dir, OutDir, "authorized_keys")
1277 if 'BSMTP' in ExtraList:
1278 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1280 if 'PRIVATE' in ExtraList:
1281 DoLink(global_dir, OutDir, "debian-private")
1283 if 'GITOLITE' in ExtraList:
1284 DoLink(global_dir, OutDir, "ssh-gitolite")
1285 if 'exportOptions' in host[1]:
1286 for entry in host[1]['exportOptions']:
1287 v = entry.split('=',1)
1288 if v[0] != 'GITOLITE' or len(v) != 2: continue
1289 gitolite_accounts = filter(lambda x: IsInGroup(x, [v[1]], current_host), all_accounts)
1290 gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1291 GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(v[1],))
1293 if 'WEB-PASSWORDS' in ExtraList:
1294 DoLink(global_dir, OutDir, "web-passwords")
1296 if 'VOIP-PASSWORDS' in ExtraList:
1297 DoLink(global_dir, OutDir, "voip-passwords")
1299 if 'KEYRING' in ExtraList:
1301 bn = os.path.basename(k)
1302 if os.path.isdir(k):
1303 src = os.path.join(global_dir, bn)
1304 replaceTree(src, OutDir)
1306 DoLink(global_dir, OutDir, bn)
1310 bn = os.path.basename(k)
1311 target = os.path.join(OutDir, bn)
1312 if os.path.isdir(target):
1315 posix.remove(target)
1318 DoLink(global_dir, OutDir, "last_update.trace")
1321 def getLastLDAPChangeTime(l):
1322 mods = l.search_s('cn=log',
1323 ldap.SCOPE_ONELEVEL,
1324 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1329 # Sort the list by reqEnd
1330 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1331 # Take the last element in the array
1332 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1336 def getLastKeyringChangeTime():
1339 mt = os.path.getmtime(k)
1345 def getLastBuildTime(gdir):
1346 cache_last_ldap_mod = 0
1347 cache_last_unix_mod = 0
1351 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1352 cache_last_mod=fd.read().split()
1354 cache_last_ldap_mod = cache_last_mod[0]
1355 cache_last_unix_mod = int(cache_last_mod[1])
1356 cache_last_run = int(cache_last_mod[2])
1357 except IndexError, ValueError:
1361 if e.errno == errno.ENOENT:
1366 return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1369 parser = optparse.OptionParser()
1370 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1371 help="Output directory.")
1372 parser.add_option("-f", "--force", dest="force", action="store_true",
1373 help="Force generation, even if no update to LDAP has happened.")
1375 (options, args) = parser.parse_args()
1380 if options.generatedir is not None:
1381 generate_dir = os.environ['UD_GENERATEDIR']
1382 elif 'UD_GENERATEDIR' in os.environ:
1383 generate_dir = os.environ['UD_GENERATEDIR']
1385 generate_dir = GenerateDir
1388 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1389 lock = get_lock( lockf )
1391 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1394 l = make_ldap_conn()
1396 time_started = int(time.time())
1397 ldap_last_mod = getLastLDAPChangeTime(l)
1398 unix_last_mod = getLastKeyringChangeTime()
1399 cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1401 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)
1403 if not options.force and not need_update:
1404 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1405 fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1409 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1410 generate_all(generate_dir, l)
1411 tracefd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, time_started))
1415 if __name__ == "__main__":
1416 if 'UD_PROFILE' in os.environ:
1419 cProfile.run('ud_generate()', "udg_prof")
1420 p = pstats.Stats('udg_prof')
1421 ##p.sort_stats('time').print_stats()
1422 p.sort_stats('cumulative').print_stats()
1428 # vim:set shiftwidth=3: