3 # Generates passwd, shadow and group files from the ldap directory.
5 # Copyright (c) 2000-2001 Jason Gunthorpe <jgg@debian.org>
6 # Copyright (c) 2003-2004 James Troup <troup@debian.org>
7 # Copyright (c) 2004-2005,7 Joey Schulze <joey@infodrom.org>
8 # Copyright (c) 2001-2007 Ryan Murray <rmurray@debian.org>
9 # Copyright (c) 2008,2009,2010,2011 Peter Palfrader <peter@palfrader.org>
10 # Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
11 # Copyright (c) 2008 Mark Hymers <mhy@debian.org>
12 # Copyright (c) 2008 Luk Claes <luk@debian.org>
13 # Copyright (c) 2008 Thomas Viehmann <tv@beamnet.de>
14 # Copyright (c) 2009 Stephen Gran <steve@lobefin.net>
15 # Copyright (c) 2010 Helmut Grohne <helmut@subdivi.de>
17 # This program is free software; you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation; either version 2 of the License, or
20 # (at your option) any later version.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
31 import string, re, time, ldap, optparse, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp, fcntl
32 from userdir_ldap import *
33 from userdir_exceptions import *
35 from xml.etree.ElementTree import Element, SubElement, Comment
36 from xml.etree import ElementTree
37 from xml.dom import minidom
39 from cStringIO import StringIO
41 from StringIO import StringIO
43 import simplejson as json
46 if not '__author__' in json.__dict__:
47 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
48 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
51 sys.stderr.write("You should probably not run ud-generate as root.\n")
63 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
65 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)(,\s*([^ <>@]+@[^ ,<>@]+))*$")
66 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
67 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
68 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
69 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
70 isSSHFP = re.compile("^\s*IN\s+SSHFP")
71 DNSZone = ".debian.net"
72 Keyrings = ConfModule.sync_keyrings.split(":")
73 GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
74 MX_remap = json.loads(ConfModule.MX_remap)
77 """Return a pretty-printed XML string for the Element.
79 rough_string = ElementTree.tostring(elem, 'utf-8')
80 reparsed = minidom.parseString(rough_string)
81 return reparsed.toprettyxml(indent=" ")
83 def safe_makedirs(dir):
87 if e.errno == errno.EEXIST:
96 if e.errno == errno.ENOENT:
101 def get_lock(fn, wait=5*60):
104 ends = time.time() + wait
109 fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
113 if time.time() >= ends:
115 sl = min(sl*2, 10, ends - time.time())
121 return Str.translate(string.maketrans("\n\r\t", "$$$"))
123 def DoLink(From, To, File):
125 posix.remove(To + File)
128 posix.link(From + File, To + File)
130 def IsRetired(account):
132 Looks for accountStatus in the LDAP record and tries to
133 match it against one of the known retired statuses
136 status = account['accountStatus']
138 line = status.split()
141 if status == "inactive":
144 elif status == "memorial":
147 elif status == "retiring":
148 # We'll give them a few extra days over what we said
149 age = 6 * 31 * 24 * 60 * 60
151 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
159 #def IsGidDebian(account):
160 # return account['gidNumber'] == 800
162 # See if this user is in the group list
163 def IsInGroup(account, allowed, current_host):
164 # See if the primary group is in the list
165 if str(account['gidNumber']) in allowed: return True
167 # Check the host based ACL
168 if account.is_allowed_by_hostacl(current_host): return True
170 # See if there are supplementary groups
171 if not 'supplementaryGid' in account: return False
174 addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
180 def Die(File, F, Fdb):
186 os.remove(File + ".tmp")
190 os.remove(File + ".tdb.tmp")
194 def Done(File, F, Fdb):
197 os.rename(File + ".tmp", File)
200 os.rename(File + ".tdb.tmp", File + ".tdb")
202 # Generate the password list
203 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
206 F = open(File + ".tdb.tmp", "w")
211 # Do not let people try to buffer overflow some busted passwd parser.
212 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
214 userlist[a['uid']] = a['gidNumber']
215 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
221 HomePrefix, a['uid'],
223 line = Sanitize(line) + "\n"
224 F.write("0%u %s" % (i, line))
225 F.write(".%s %s" % (a['uid'], line))
226 F.write("=%d %s" % (a['uidNumber'], line))
229 # Oops, something unspeakable happened.
235 # Return the list of users so we know which keys to export
238 def GenAllUsers(accounts, file):
241 OldMask = os.umask(0022)
242 f = open(file + ".tmp", "w", 0644)
247 all.append( { 'uid': a['uid'],
248 'uidNumber': a['uidNumber'],
249 'active': a.pw_active() and a.shadow_active() } )
252 # Oops, something unspeakable happened.
258 # Generate the shadow list
259 def GenShadow(accounts, File):
262 OldMask = os.umask(0077)
263 F = open(File + ".tdb.tmp", "w", 0600)
268 # If the account is locked, mark it as such in shadow
269 # See Debian Bug #308229 for why we set it to 1 instead of 0
270 if not a.pw_active(): ShadowExpire = '1'
271 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
272 else: ShadowExpire = ''
275 values.append(a['uid'])
276 values.append(a.get_password())
277 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
278 if key in a: values.append(a[key])
279 else: values.append('')
280 values.append(ShadowExpire)
281 line = ':'.join(values)+':'
282 line = Sanitize(line) + "\n"
283 F.write("0%u %s" % (i, line))
284 F.write(".%s %s" % (a['uid'], line))
287 # Oops, something unspeakable happened.
293 # Generate the sudo passwd file
294 def GenShadowSudo(accounts, File, untrusted, current_host):
297 OldMask = os.umask(0077)
298 F = open(File + ".tmp", "w", 0600)
303 if 'sudoPassword' in a:
304 for entry in a['sudoPassword']:
305 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
308 uuid = Match.group(1)
309 status = Match.group(2)
310 hosts = Match.group(3)
311 cryptedpass = Match.group(4)
313 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
315 for_all = hosts == "*"
316 for_this_host = current_host in hosts.split(',')
317 if not (for_all or for_this_host):
319 # ignore * passwords for untrusted hosts, but copy host specific passwords
320 if for_all and untrusted:
323 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
328 Line = "%s:%s" % (a['uid'], Pass)
329 Line = Sanitize(Line) + "\n"
330 F.write("%s" % (Line))
332 # Oops, something unspeakable happened.
338 # Generate the sudo passwd file
339 def GenSSHGitolite(accounts, File):
342 OldMask = os.umask(0022)
343 F = open(File + ".tmp", "w", 0600)
346 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
348 if not 'sshRSAAuthKey' in a: continue
351 prefix = GitoliteSSHRestrictions.replace('@@USER@@', User)
352 for I in a["sshRSAAuthKey"]:
353 if I.startswith('ssh-'):
354 line = "%s %s"%(prefix, I)
356 line = "%s,%s"%(prefix, I)
357 line = Sanitize(line) + "\n"
360 # Oops, something unspeakable happened.
366 # Generate the shadow list
367 def GenSSHShadow(global_dir, accounts):
368 # Fetch all the users
372 if not 'sshRSAAuthKey' in a: continue
375 for I in a['sshRSAAuthKey']:
376 MultipleLine = "%s" % I
377 MultipleLine = Sanitize(MultipleLine)
378 contents.append(MultipleLine)
379 userkeys[a['uid']] = contents
382 # Generate the webPassword list
383 def GenWebPassword(accounts, File):
386 OldMask = os.umask(0077)
387 F = open(File, "w", 0600)
391 if not 'webPassword' in a: continue
392 if not a.pw_active(): continue
394 Pass = str(a['webPassword'])
395 Line = "%s:%s" % (a['uid'], Pass)
396 Line = Sanitize(Line) + "\n"
397 F.write("%s" % (Line))
403 # Generate the voipPassword list
404 def GenVoipPassword(accounts, File):
407 OldMask = os.umask(0077)
408 F = open(File, "w", 0600)
411 root = Element('include')
414 if not 'voipPassword' in a: continue
415 if not a.pw_active(): continue
417 Pass = str(a['voipPassword'])
418 user = Element('user')
419 user.attrib['id'] = "%s" % (a['uid'])
421 params = Element('params')
423 param = Element('param')
425 param.attrib['name'] = "a1-hash"
426 param.attrib['value'] = "%s" % (Pass)
427 variables = Element('variables')
428 user.append(variables)
429 variable = Element('variable')
430 variable.attrib['name'] = "toll_allow"
431 variable.attrib['value'] = "domestic,international,local"
432 variables.append(variable)
434 F.write("%s" % (prettify(root)))
441 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
442 OldMask = os.umask(0077)
443 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
446 if f not in ssh_userkeys:
448 # If we're not exporting their primary group, don't export
451 if userlist[f] in grouprevmap.keys():
452 grname = grouprevmap[userlist[f]]
455 if int(userlist[f]) <= 100:
456 # In these cases, look it up in the normal way so we
457 # deal with cases where, for instance, users are in group
458 # users as their primary group.
459 grname = grp.getgrgid(userlist[f])[0]
464 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])
468 for line in ssh_userkeys[f]:
469 if line.startswith("allowed_hosts=") and ' ' in line:
470 machines, line = line.split('=', 1)[1].split(' ', 1)
471 if current_host not in machines.split(','):
472 continue # skip this key
475 continue # no keys for this host
476 contents = "\n".join(lines) + "\n"
478 to = tarfile.TarInfo(name=f)
479 # These will only be used where the username doesn't
480 # exist on the target system for some reason; hence,
481 # in those cases, the safest thing is for the file to
482 # be owned by root but group nobody. This deals with
483 # the bloody obscure case where the group fails to exist
484 # whilst the user does (in which case we want to avoid
485 # ending up with a file which is owned user:root to avoid
486 # a fairly obvious attack vector)
489 # Using the username / groupname fields avoids any need
490 # to give a shit^W^W^Wcare about the UIDoffset stuff.
494 to.mtime = int(time.time())
495 to.size = len(contents)
497 tf.addfile(to, StringIO(contents))
500 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
502 # add a list of groups to existing groups,
503 # including all subgroups thereof, recursively.
504 # basically this proceduces the transitive hull of the groups in
506 def addGroups(existingGroups, newGroups, uid, current_host):
507 for group in newGroups:
508 # if it's a <group>@host, split it and verify it's on the current host.
509 s = group.split('@', 1)
510 if len(s) == 2 and s[1] != current_host:
514 # let's see if we handled this group already
515 if group in existingGroups:
518 if not GroupIDMap.has_key(group):
519 print "Group", group, "does not exist but", uid, "is in it"
522 existingGroups.append(group)
524 if SubGroupMap.has_key(group):
525 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
527 # Generate the group list
528 def GenGroup(accounts, File, current_host):
532 F = open(File + ".tdb.tmp", "w")
534 # Generate the GroupMap
538 GroupHasPrimaryMembers = {}
540 # Sort them into a list of groups having a set of users
542 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
543 if not 'supplementaryGid' in a: continue
546 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
548 GroupMap[g].append(a['uid'])
550 # Output the group file.
552 for x in GroupMap.keys():
553 if not x in GroupIDMap:
556 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
559 grouprevmap[GroupIDMap[x]] = x
561 Line = "%s:x:%u:" % (x, GroupIDMap[x])
563 for I in GroupMap[x]:
564 Line = Line + ("%s%s" % (Comma, I))
566 Line = Sanitize(Line) + "\n"
567 F.write("0%u %s" % (J, Line))
568 F.write(".%s %s" % (x, Line))
569 F.write("=%u %s" % (GroupIDMap[x], Line))
572 # Oops, something unspeakable happened.
580 def CheckForward(accounts):
582 if not 'emailForward' in a: continue
586 # Do not allow people to try to buffer overflow busted parsers
587 if len(a['emailForward']) > 200: delete = True
588 # Check the forwarding address
589 elif EmailCheck.match(a['emailForward']) is None: delete = True
592 a.delete_mailforward()
594 # Generate the email forwarding list
595 def GenForward(accounts, File):
598 OldMask = os.umask(0022)
599 F = open(File + ".tmp", "w", 0644)
603 if not 'emailForward' in a: continue
604 Line = "%s: %s" % (a['uid'], a['emailForward'])
605 Line = Sanitize(Line) + "\n"
608 # Oops, something unspeakable happened.
614 def GenCDB(accounts, File, key):
617 OldMask = os.umask(0022)
618 # nothing else does the fsync stuff, so why do it here?
619 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
620 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
623 # Write out the email address for each user
625 if not key in a: continue
628 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
631 # Oops, something unspeakable happened.
635 if Fdb.close() != None:
636 raise "cdbmake gave an error"
638 # Generate the anon XEarth marker file
639 def GenMarkers(accounts, File):
642 F = open(File + ".tmp", "w")
644 # Write out the position for each user
646 if not ('latitude' in a and 'longitude' in a): continue
648 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
649 Line = Sanitize(Line) + "\n"
654 # Oops, something unspeakable happened.
660 # Generate the debian-private subscription list
661 def GenPrivate(accounts, File):
664 F = open(File + ".tmp", "w")
666 # Write out the position for each user
668 if not a.is_active_user(): continue
669 if a.is_guest_account(): continue
670 if not 'privateSub' in a: continue
672 Line = "%s"%(a['privateSub'])
673 Line = Sanitize(Line) + "\n"
678 # Oops, something unspeakable happened.
684 # Generate a list of locked accounts
685 def GenDisabledAccounts(accounts, File):
688 F = open(File + ".tmp", "w")
689 disabled_accounts = []
691 # Fetch all the users
693 if a.pw_active(): continue
694 Line = "%s:%s" % (a['uid'], "Account is locked")
695 disabled_accounts.append(a)
696 F.write(Sanitize(Line) + "\n")
698 # Oops, something unspeakable happened.
703 return disabled_accounts
705 # Generate the list of local addresses that refuse all mail
706 def GenMailDisable(accounts, File):
709 F = open(File + ".tmp", "w")
712 if not 'mailDisableMessage' in a: continue
713 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
714 Line = Sanitize(Line) + "\n"
717 # Oops, something unspeakable happened.
723 # Generate a list of uids that should have boolean affects applied
724 def GenMailBool(accounts, File, key):
727 F = open(File + ".tmp", "w")
730 if not key in a: continue
731 if not a[key] == 'TRUE': continue
732 Line = "%s"%(a['uid'])
733 Line = Sanitize(Line) + "\n"
736 # Oops, something unspeakable happened.
742 # Generate a list of hosts for RBL or whitelist purposes.
743 def GenMailList(accounts, File, key):
746 F = open(File + ".tmp", "w")
748 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
749 else: validregex = re.compile('^[-\w.]+$')
752 if not key in a: continue
754 filtered = filter(lambda z: validregex.match(z), a[key])
755 if len(filtered) == 0: continue
756 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
757 line = a['uid'] + ': ' + ' : '.join(filtered)
758 line = Sanitize(line) + "\n"
761 # Oops, something unspeakable happened.
767 def isRoleAccount(account):
768 return 'debianRoleAccount' in account['objectClass']
770 # Generate the DNS Zone file
771 def GenDNS(accounts, File):
774 F = open(File + ".tmp", "w")
776 # Fetch all the users
779 # Write out the zone file entry for each user
781 if not 'dnsZoneEntry' in a: continue
782 if not a.is_active_user() and not isRoleAccount(a): continue
783 if a.is_guest_account(): continue
786 F.write("; %s\n"%(a.email_address()))
787 for z in a["dnsZoneEntry"]:
788 Split = z.lower().split()
789 if Split[1].lower() == 'in':
790 Line = " ".join(Split) + "\n"
793 Host = Split[0] + DNSZone
794 if BSMTPCheck.match(Line) != None:
795 F.write("; Has BSMTP\n")
797 # Write some identification information
798 if not RRs.has_key(Host):
799 if Split[2].lower() in ["a", "aaaa"]:
800 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
801 for y in a["keyFingerPrint"]:
802 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
806 Line = "; Err %s"%(str(Split))
811 F.write("; Errors:\n")
812 for line in str(e).split("\n"):
813 F.write("; %s\n"%(line))
816 # Oops, something unspeakable happened.
822 def ExtractDNSInfo(x):
826 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
829 if x[1].has_key("ipHostNumber"):
830 for I in x[1]["ipHostNumber"]:
831 if IsV6Addr.match(I) != None:
832 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
834 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
838 if 'sshRSAHostKey' in x[1]:
839 for I in x[1]["sshRSAHostKey"]:
841 if Split[0] == 'ssh-rsa':
843 if Split[0] == 'ssh-dss':
845 if Algorithm == None:
847 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
848 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
850 if 'architecture' in x[1]:
851 Arch = GetAttr(x, "architecture")
853 if x[1].has_key("machine"):
854 Mach = " " + GetAttr(x, "machine")
855 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
857 if x[1].has_key("mXRecord"):
858 for I in x[1]["mXRecord"]:
860 for e in MX_remap[I]:
861 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
863 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
867 # Generate the DNS records
868 def GenZoneRecords(host_attrs, File):
871 F = open(File + ".tmp", "w")
873 # Fetch all the hosts
875 if x[1].has_key("hostname") == 0:
878 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
881 DNSInfo = ExtractDNSInfo(x)
885 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
888 Line = "\t\t\t%s" % (Line)
892 # this would write sshfp lines for services on machines
893 # but we can't yet, since some are cnames and we'll make
894 # an invalid zonefile
896 # for i in x[1].get("purpose", []):
897 # m = PurposeHostField.match(i)
900 # # we ignore [[*..]] entries
901 # if m.startswith('*'):
903 # if m.startswith('-'):
906 # if not m.endswith(HostDomain):
908 # if not m.endswith('.'):
910 # for Line in DNSInfo:
911 # if isSSHFP.match(Line):
912 # Line = "%s\t%s" % (m, Line)
913 # F.write(Line + "\n")
915 # Oops, something unspeakable happened.
921 # Generate the BSMTP file
922 def GenBSMTP(accounts, File, HomePrefix):
925 F = open(File + ".tmp", "w")
927 # Write out the zone file entry for each user
929 if not 'dnsZoneEntry' in a: continue
930 if not a.is_active_user(): continue
933 for z in a["dnsZoneEntry"]:
934 Split = z.lower().split()
935 if Split[1].lower() == 'in':
936 for y in range(0, len(Split)):
939 Line = " ".join(Split) + "\n"
941 Host = Split[0] + DNSZone
942 if BSMTPCheck.match(Line) != None:
943 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
944 a['uid'], HomePrefix, a['uid'], Host))
947 F.write("; Errors\n")
950 # Oops, something unspeakable happened.
956 def HostToIP(Host, mapped=True):
960 if Host[1].has_key("ipHostNumber"):
961 for addr in Host[1]["ipHostNumber"]:
962 IPAdresses.append(addr)
963 if IsV6Addr.match(addr) is None and mapped == "True":
964 IPAdresses.append("::ffff:"+addr)
968 # Generate the ssh known hosts file
969 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
972 OldMask = os.umask(0022)
973 F = open(File + ".tmp", "w", 0644)
977 if x[1].has_key("hostname") == 0 or \
978 x[1].has_key("sshRSAHostKey") == 0:
980 Host = GetAttr(x, "hostname")
982 if Host.endswith(HostDomain):
983 HostNames.append(Host[:-(len(HostDomain) + 1)])
985 # in the purpose field [[host|some other text]] (where some other text is optional)
986 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
987 # file. But so that we don't have to add everything we link we can add an asterisk
988 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
989 # http linking it we also support [[-hostname]] entries.
990 for i in x[1].get("purpose", []):
991 m = PurposeHostField.match(i)
994 # we ignore [[*..]] entries
995 if m.startswith('*'):
997 if m.startswith('-'):
1001 if m.endswith(HostDomain):
1002 HostNames.append(m[:-(len(HostDomain) + 1)])
1004 for I in x[1]["sshRSAHostKey"]:
1005 if mode and mode == 'authorized_keys':
1007 if 'sshdistAuthKeysHost' in x[1]:
1008 hosts += x[1]['sshdistAuthKeysHost']
1009 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1010 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1011 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1013 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1014 Line = Sanitize(Line) + "\n"
1016 # Oops, something unspeakable happened.
1022 # Generate the debianhosts file (list of all IP addresses)
1023 def GenHosts(host_attrs, File):
1026 OldMask = os.umask(0022)
1027 F = open(File + ".tmp", "w", 0644)
1032 for x in host_attrs:
1034 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1037 if not 'ipHostNumber' in x[1]:
1040 addrs = x[1]["ipHostNumber"]
1042 if addr not in seen:
1044 addr = Sanitize(addr) + "\n"
1047 # Oops, something unspeakable happened.
1053 def replaceTree(src, dst_basedir):
1054 bn = os.path.basename(src)
1055 dst = os.path.join(dst_basedir, bn)
1057 shutil.copytree(src, dst)
1059 def GenKeyrings(OutDir):
1061 if os.path.isdir(k):
1062 replaceTree(k, OutDir)
1064 shutil.copy(k, OutDir)
1067 def get_accounts(ldap_conn):
1068 # Fetch all the users
1069 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1070 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1071 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1072 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1073 "shadowExpire", "emailForward", "latitude", "longitude",\
1074 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1075 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1076 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1077 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1078 "mailContentInspectionAction", "webPassword", "voipPassword"])
1080 if passwd_attrs is None:
1081 raise UDEmptyList, "No Users"
1082 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1083 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1087 def get_hosts(ldap_conn):
1088 # Fetch all the hosts
1089 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1090 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1091 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1093 if HostAttrs == None:
1094 raise UDEmptyList, "No Hosts"
1096 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1101 def make_ldap_conn():
1102 # Connect to the ldap server
1104 # for testing purposes it's sometimes useful to pass username/password
1105 # via the environment
1106 if 'UD_CREDENTIALS' in os.environ:
1107 Pass = os.environ['UD_CREDENTIALS'].split()
1109 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1110 Pass = F.readline().strip().split(" ")
1112 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1118 def setup_group_maps(l):
1119 # Fetch all the groups
1122 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1123 ["gid", "gidNumber", "subGroup"])
1125 # Generate the subgroup_map and group_id_map
1127 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1129 if x[1].has_key("gidNumber") == 0:
1131 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1132 if x[1].has_key("subGroup") != 0:
1133 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1137 SubGroupMap = subgroup_map
1138 GroupIDMap = group_id_map
1140 def generate_all(global_dir, ldap_conn):
1141 accounts = get_accounts(ldap_conn)
1142 host_attrs = get_hosts(ldap_conn)
1145 # Generate global things
1146 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1148 accounts = filter(lambda x: not IsRetired(x), accounts)
1149 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1151 CheckForward(accounts)
1153 GenMailDisable(accounts, global_dir + "mail-disable")
1154 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1155 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1156 GenPrivate(accounts, global_dir + "debian-private")
1157 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1158 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1159 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1160 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1161 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1162 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1163 GenWebPassword(accounts, global_dir + "web-passwords")
1164 GenVoipPassword(accounts, global_dir + "voip-passwords")
1165 GenKeyrings(global_dir)
1168 GenForward(accounts, global_dir + "forward-alias")
1170 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1171 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1173 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1174 GenMarkers(accounts, global_dir + "markers")
1175 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1176 GenHosts(host_attrs, global_dir + "debianhosts")
1177 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1179 GenDNS(accounts, global_dir + "dns-zone")
1180 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1182 setup_group_maps(ldap_conn)
1184 for host in host_attrs:
1185 if not "hostname" in host[1]:
1187 generate_host(host, global_dir, accounts, ssh_userkeys)
1189 def generate_host(host, global_dir, all_accounts, ssh_userkeys):
1190 current_host = host[1]['hostname'][0]
1191 OutDir = global_dir + current_host + '/'
1192 if not os.path.isdir(OutDir):
1195 # Get the group list and convert any named groups to numerics
1197 for groupname in AllowedGroupsPreload.strip().split(" "):
1198 GroupList[groupname] = True
1199 if 'allowedGroups' in host[1]:
1200 for groupname in host[1]['allowedGroups']:
1201 GroupList[groupname] = True
1202 for groupname in GroupList.keys():
1203 if groupname in GroupIDMap:
1204 GroupList[str(GroupIDMap[groupname])] = True
1207 if 'exportOptions' in host[1]:
1208 for extra in host[1]['exportOptions']:
1209 ExtraList[extra.upper()] = True
1212 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1214 DoLink(global_dir, OutDir, "debianhosts")
1215 DoLink(global_dir, OutDir, "ssh_known_hosts")
1216 DoLink(global_dir, OutDir, "disabled-accounts")
1219 if 'NOPASSWD' in ExtraList:
1220 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1222 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1224 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1225 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1227 # Now we know who we're allowing on the machine, export
1228 # the relevant ssh keys
1229 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1231 if not 'NOPASSWD' in ExtraList:
1232 GenShadow(accounts, OutDir + "shadow")
1234 # Link in global things
1235 if not 'NOMARKERS' in ExtraList:
1236 DoLink(global_dir, OutDir, "markers")
1237 DoLink(global_dir, OutDir, "mail-forward.cdb")
1238 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1239 DoLink(global_dir, OutDir, "mail-disable")
1240 DoLink(global_dir, OutDir, "mail-greylist")
1241 DoLink(global_dir, OutDir, "mail-callout")
1242 DoLink(global_dir, OutDir, "mail-rbl")
1243 DoLink(global_dir, OutDir, "mail-rhsbl")
1244 DoLink(global_dir, OutDir, "mail-whitelist")
1245 DoLink(global_dir, OutDir, "all-accounts.json")
1246 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1247 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1248 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1251 DoLink(global_dir, OutDir, "forward-alias")
1253 if 'DNS' in ExtraList:
1254 DoLink(global_dir, OutDir, "dns-zone")
1255 DoLink(global_dir, OutDir, "dns-sshfp")
1257 if 'AUTHKEYS' in ExtraList:
1258 DoLink(global_dir, OutDir, "authorized_keys")
1260 if 'BSMTP' in ExtraList:
1261 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1263 if 'PRIVATE' in ExtraList:
1264 DoLink(global_dir, OutDir, "debian-private")
1266 if 'GITOLITE' in ExtraList:
1267 DoLink(global_dir, OutDir, "ssh-gitolite")
1268 if 'exportOptions' in host[1]:
1269 for entry in host[1]['exportOptions']:
1270 v = entry.split('=',1)
1271 if v[0] != 'GITOLITE' or len(v) != 2: continue
1272 gitolite_accounts = filter(lambda x: IsInGroup(x, [v[1]], current_host), all_accounts)
1273 GenSSHGitolite(gitolite_accounts, OutDir + "ssh-gitolite-%s"%(v[1],))
1275 if 'WEB-PASSWORDS' in ExtraList:
1276 DoLink(global_dir, OutDir, "web-passwords")
1278 if 'VOIP-PASSWORDS' in ExtraList:
1279 DoLink(global_dir, OutDir, "voip-passwords")
1281 if 'KEYRING' in ExtraList:
1283 bn = os.path.basename(k)
1284 if os.path.isdir(k):
1285 src = os.path.join(global_dir, bn)
1286 replaceTree(src, OutDir)
1288 DoLink(global_dir, OutDir, bn)
1292 bn = os.path.basename(k)
1293 target = os.path.join(OutDir, bn)
1294 if os.path.isdir(target):
1297 posix.remove(target)
1300 DoLink(global_dir, OutDir, "last_update.trace")
1303 def getLastLDAPChangeTime(l):
1304 mods = l.search_s('cn=log',
1305 ldap.SCOPE_ONELEVEL,
1306 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1311 # Sort the list by reqEnd
1312 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1313 # Take the last element in the array
1314 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1318 def getLastKeyringChangeTime():
1321 mt = os.path.getmtime(k)
1327 def getLastBuildTime(gdir):
1328 cache_last_ldap_mod = 0
1329 cache_last_unix_mod = 0
1332 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1333 cache_last_mod=fd.read().split()
1335 cache_last_ldap_mod = cache_last_mod[0]
1336 cache_last_unix_mod = int(cache_last_mod[1])
1337 except IndexError, ValueError:
1341 if e.errno == errno.ENOENT:
1346 return (cache_last_ldap_mod, cache_last_unix_mod)
1349 parser = optparse.OptionParser()
1350 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1351 help="Output directory.")
1352 parser.add_option("-f", "--force", dest="force", action="store_true",
1353 help="Force generation, even if no update to LDAP has happened.")
1355 (options, args) = parser.parse_args()
1360 if options.generatedir is not None:
1361 generate_dir = os.environ['UD_GENERATEDIR']
1362 elif 'UD_GENERATEDIR' in os.environ:
1363 generate_dir = os.environ['UD_GENERATEDIR']
1365 generate_dir = GenerateDir
1368 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1369 lock = get_lock( lockf )
1371 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1374 l = make_ldap_conn()
1376 time_started = int(time.time())
1377 ldap_last_mod = getLastLDAPChangeTime(l)
1378 unix_last_mod = getLastKeyringChangeTime()
1379 cache_last_ldap_mod, cache_last_unix_mod = getLastBuildTime(generate_dir)
1381 need_update = (ldap_last_mod > cache_last_ldap_mod) or (unix_last_mod > cache_last_unix_mod)
1383 if not options.force and not need_update:
1384 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1385 fd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1389 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1390 generate_all(generate_dir, l)
1391 tracefd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1395 if __name__ == "__main__":
1396 if 'UD_PROFILE' in os.environ:
1399 cProfile.run('ud_generate()', "udg_prof")
1400 p = pstats.Stats('udg_prof')
1401 ##p.sort_stats('time').print_stats()
1402 p.sort_stats('cumulative').print_stats()
1408 # vim:set shiftwidth=3: