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 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.
824 socket.inet_pton(socket.AF_INET6, i)
829 def ExtractDNSInfo(x):
833 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
836 if x[1].has_key("ipHostNumber"):
837 for I in x[1]["ipHostNumber"]:
839 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
841 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
845 if 'sshRSAHostKey' in x[1]:
846 for I in x[1]["sshRSAHostKey"]:
848 if Split[0] == 'ssh-rsa':
850 if Split[0] == 'ssh-dss':
852 if Algorithm == None:
854 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
855 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
857 if 'architecture' in x[1]:
858 Arch = GetAttr(x, "architecture")
860 if x[1].has_key("machine"):
861 Mach = " " + GetAttr(x, "machine")
862 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
864 if x[1].has_key("mXRecord"):
865 for I in x[1]["mXRecord"]:
867 for e in MX_remap[I]:
868 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
870 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
874 # Generate the DNS records
875 def GenZoneRecords(host_attrs, File):
878 F = open(File + ".tmp", "w")
880 # Fetch all the hosts
882 if x[1].has_key("hostname") == 0:
885 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
888 DNSInfo = ExtractDNSInfo(x)
892 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
895 Line = "\t\t\t%s" % (Line)
899 # this would write sshfp lines for services on machines
900 # but we can't yet, since some are cnames and we'll make
901 # an invalid zonefile
903 # for i in x[1].get("purpose", []):
904 # m = PurposeHostField.match(i)
907 # # we ignore [[*..]] entries
908 # if m.startswith('*'):
910 # if m.startswith('-'):
913 # if not m.endswith(HostDomain):
915 # if not m.endswith('.'):
917 # for Line in DNSInfo:
918 # if isSSHFP.match(Line):
919 # Line = "%s\t%s" % (m, Line)
920 # F.write(Line + "\n")
922 # Oops, something unspeakable happened.
928 # Generate the BSMTP file
929 def GenBSMTP(accounts, File, HomePrefix):
932 F = open(File + ".tmp", "w")
934 # Write out the zone file entry for each user
936 if not 'dnsZoneEntry' in a: continue
937 if not a.is_active_user(): continue
940 for z in a["dnsZoneEntry"]:
941 Split = z.lower().split()
942 if Split[1].lower() == 'in':
943 for y in range(0, len(Split)):
946 Line = " ".join(Split) + "\n"
948 Host = Split[0] + DNSZone
949 if BSMTPCheck.match(Line) != None:
950 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
951 a['uid'], HomePrefix, a['uid'], Host))
954 F.write("; Errors\n")
957 # Oops, something unspeakable happened.
963 def HostToIP(Host, mapped=True):
967 if Host[1].has_key("ipHostNumber"):
968 for addr in Host[1]["ipHostNumber"]:
969 IPAdresses.append(addr)
970 if not is_ipv6_addr(addr) and mapped == "True":
971 IPAdresses.append("::ffff:"+addr)
975 # Generate the ssh known hosts file
976 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
979 OldMask = os.umask(0022)
980 F = open(File + ".tmp", "w", 0644)
984 if x[1].has_key("hostname") == 0 or \
985 x[1].has_key("sshRSAHostKey") == 0:
987 Host = GetAttr(x, "hostname")
989 if Host.endswith(HostDomain):
990 HostNames.append(Host[:-(len(HostDomain) + 1)])
992 # in the purpose field [[host|some other text]] (where some other text is optional)
993 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
994 # file. But so that we don't have to add everything we link we can add an asterisk
995 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
996 # http linking it we also support [[-hostname]] entries.
997 for i in x[1].get("purpose", []):
998 m = PurposeHostField.match(i)
1001 # we ignore [[*..]] entries
1002 if m.startswith('*'):
1004 if m.startswith('-'):
1008 if m.endswith(HostDomain):
1009 HostNames.append(m[:-(len(HostDomain) + 1)])
1011 for I in x[1]["sshRSAHostKey"]:
1012 if mode and mode == 'authorized_keys':
1014 if 'sshdistAuthKeysHost' in x[1]:
1015 hosts += x[1]['sshdistAuthKeysHost']
1016 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1017 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1018 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1020 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1021 Line = Sanitize(Line) + "\n"
1023 # Oops, something unspeakable happened.
1029 # Generate the debianhosts file (list of all IP addresses)
1030 def GenHosts(host_attrs, File):
1033 OldMask = os.umask(0022)
1034 F = open(File + ".tmp", "w", 0644)
1039 for x in host_attrs:
1041 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1044 if not 'ipHostNumber' in x[1]:
1047 addrs = x[1]["ipHostNumber"]
1049 if addr not in seen:
1051 addr = Sanitize(addr) + "\n"
1054 # Oops, something unspeakable happened.
1060 def replaceTree(src, dst_basedir):
1061 bn = os.path.basename(src)
1062 dst = os.path.join(dst_basedir, bn)
1064 shutil.copytree(src, dst)
1066 def GenKeyrings(OutDir):
1068 if os.path.isdir(k):
1069 replaceTree(k, OutDir)
1071 shutil.copy(k, OutDir)
1074 def get_accounts(ldap_conn):
1075 # Fetch all the users
1076 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1077 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1078 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1079 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1080 "shadowExpire", "emailForward", "latitude", "longitude",\
1081 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1082 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1083 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1084 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1085 "mailContentInspectionAction", "webPassword", "voipPassword"])
1087 if passwd_attrs is None:
1088 raise UDEmptyList, "No Users"
1089 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1090 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1094 def get_hosts(ldap_conn):
1095 # Fetch all the hosts
1096 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1097 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1098 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1100 if HostAttrs == None:
1101 raise UDEmptyList, "No Hosts"
1103 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1108 def make_ldap_conn():
1109 # Connect to the ldap server
1111 # for testing purposes it's sometimes useful to pass username/password
1112 # via the environment
1113 if 'UD_CREDENTIALS' in os.environ:
1114 Pass = os.environ['UD_CREDENTIALS'].split()
1116 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1117 Pass = F.readline().strip().split(" ")
1119 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1125 def setup_group_maps(l):
1126 # Fetch all the groups
1129 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1130 ["gid", "gidNumber", "subGroup"])
1132 # Generate the subgroup_map and group_id_map
1134 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1136 if x[1].has_key("gidNumber") == 0:
1138 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1139 if x[1].has_key("subGroup") != 0:
1140 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1144 SubGroupMap = subgroup_map
1145 GroupIDMap = group_id_map
1147 def generate_all(global_dir, ldap_conn):
1148 accounts = get_accounts(ldap_conn)
1149 host_attrs = get_hosts(ldap_conn)
1152 # Generate global things
1153 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1155 accounts = filter(lambda x: not IsRetired(x), accounts)
1156 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1158 CheckForward(accounts)
1160 GenMailDisable(accounts, global_dir + "mail-disable")
1161 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1162 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1163 GenPrivate(accounts, global_dir + "debian-private")
1164 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1165 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1166 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1167 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1168 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1169 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1170 GenWebPassword(accounts, global_dir + "web-passwords")
1171 GenVoipPassword(accounts, global_dir + "voip-passwords")
1172 GenKeyrings(global_dir)
1175 GenForward(accounts, global_dir + "forward-alias")
1177 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1178 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1180 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1181 GenMarkers(accounts, global_dir + "markers")
1182 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1183 GenHosts(host_attrs, global_dir + "debianhosts")
1184 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1186 GenDNS(accounts, global_dir + "dns-zone")
1187 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1189 setup_group_maps(ldap_conn)
1191 for host in host_attrs:
1192 if not "hostname" in host[1]:
1194 generate_host(host, global_dir, accounts, ssh_userkeys)
1196 def generate_host(host, global_dir, all_accounts, ssh_userkeys):
1197 current_host = host[1]['hostname'][0]
1198 OutDir = global_dir + current_host + '/'
1199 if not os.path.isdir(OutDir):
1202 # Get the group list and convert any named groups to numerics
1204 for groupname in AllowedGroupsPreload.strip().split(" "):
1205 GroupList[groupname] = True
1206 if 'allowedGroups' in host[1]:
1207 for groupname in host[1]['allowedGroups']:
1208 GroupList[groupname] = True
1209 for groupname in GroupList.keys():
1210 if groupname in GroupIDMap:
1211 GroupList[str(GroupIDMap[groupname])] = True
1214 if 'exportOptions' in host[1]:
1215 for extra in host[1]['exportOptions']:
1216 ExtraList[extra.upper()] = True
1219 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1221 DoLink(global_dir, OutDir, "debianhosts")
1222 DoLink(global_dir, OutDir, "ssh_known_hosts")
1223 DoLink(global_dir, OutDir, "disabled-accounts")
1226 if 'NOPASSWD' in ExtraList:
1227 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1229 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1231 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1232 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1234 # Now we know who we're allowing on the machine, export
1235 # the relevant ssh keys
1236 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1238 if not 'NOPASSWD' in ExtraList:
1239 GenShadow(accounts, OutDir + "shadow")
1241 # Link in global things
1242 if not 'NOMARKERS' in ExtraList:
1243 DoLink(global_dir, OutDir, "markers")
1244 DoLink(global_dir, OutDir, "mail-forward.cdb")
1245 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1246 DoLink(global_dir, OutDir, "mail-disable")
1247 DoLink(global_dir, OutDir, "mail-greylist")
1248 DoLink(global_dir, OutDir, "mail-callout")
1249 DoLink(global_dir, OutDir, "mail-rbl")
1250 DoLink(global_dir, OutDir, "mail-rhsbl")
1251 DoLink(global_dir, OutDir, "mail-whitelist")
1252 DoLink(global_dir, OutDir, "all-accounts.json")
1253 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1254 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1255 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1258 DoLink(global_dir, OutDir, "forward-alias")
1260 if 'DNS' in ExtraList:
1261 DoLink(global_dir, OutDir, "dns-zone")
1262 DoLink(global_dir, OutDir, "dns-sshfp")
1264 if 'AUTHKEYS' in ExtraList:
1265 DoLink(global_dir, OutDir, "authorized_keys")
1267 if 'BSMTP' in ExtraList:
1268 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1270 if 'PRIVATE' in ExtraList:
1271 DoLink(global_dir, OutDir, "debian-private")
1273 if 'GITOLITE' in ExtraList:
1274 DoLink(global_dir, OutDir, "ssh-gitolite")
1275 if 'exportOptions' in host[1]:
1276 for entry in host[1]['exportOptions']:
1277 v = entry.split('=',1)
1278 if v[0] != 'GITOLITE' or len(v) != 2: continue
1279 gitolite_accounts = filter(lambda x: IsInGroup(x, [v[1]], current_host), all_accounts)
1280 GenSSHGitolite(gitolite_accounts, OutDir + "ssh-gitolite-%s"%(v[1],))
1282 if 'WEB-PASSWORDS' in ExtraList:
1283 DoLink(global_dir, OutDir, "web-passwords")
1285 if 'VOIP-PASSWORDS' in ExtraList:
1286 DoLink(global_dir, OutDir, "voip-passwords")
1288 if 'KEYRING' in ExtraList:
1290 bn = os.path.basename(k)
1291 if os.path.isdir(k):
1292 src = os.path.join(global_dir, bn)
1293 replaceTree(src, OutDir)
1295 DoLink(global_dir, OutDir, bn)
1299 bn = os.path.basename(k)
1300 target = os.path.join(OutDir, bn)
1301 if os.path.isdir(target):
1304 posix.remove(target)
1307 DoLink(global_dir, OutDir, "last_update.trace")
1310 def getLastLDAPChangeTime(l):
1311 mods = l.search_s('cn=log',
1312 ldap.SCOPE_ONELEVEL,
1313 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1318 # Sort the list by reqEnd
1319 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1320 # Take the last element in the array
1321 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1325 def getLastKeyringChangeTime():
1328 mt = os.path.getmtime(k)
1334 def getLastBuildTime(gdir):
1335 cache_last_ldap_mod = 0
1336 cache_last_unix_mod = 0
1340 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1341 cache_last_mod=fd.read().split()
1343 cache_last_ldap_mod = cache_last_mod[0]
1344 cache_last_unix_mod = int(cache_last_mod[1])
1345 cache_last_run = int(cache_last_mod[2])
1346 except IndexError, ValueError:
1350 if e.errno == errno.ENOENT:
1355 return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1358 parser = optparse.OptionParser()
1359 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1360 help="Output directory.")
1361 parser.add_option("-f", "--force", dest="force", action="store_true",
1362 help="Force generation, even if no update to LDAP has happened.")
1364 (options, args) = parser.parse_args()
1369 if options.generatedir is not None:
1370 generate_dir = os.environ['UD_GENERATEDIR']
1371 elif 'UD_GENERATEDIR' in os.environ:
1372 generate_dir = os.environ['UD_GENERATEDIR']
1374 generate_dir = GenerateDir
1377 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1378 lock = get_lock( lockf )
1380 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1383 l = make_ldap_conn()
1385 time_started = int(time.time())
1386 ldap_last_mod = getLastLDAPChangeTime(l)
1387 unix_last_mod = getLastKeyringChangeTime()
1388 cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1390 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)
1392 if not options.force and not need_update:
1393 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1394 fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1398 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1399 generate_all(generate_dir, l)
1400 tracefd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, time_started))
1404 if __name__ == "__main__":
1405 if 'UD_PROFILE' in os.environ:
1408 cProfile.run('ud_generate()', "udg_prof")
1409 p = pstats.Stats('udg_prof')
1410 ##p.sort_stats('time').print_stats()
1411 p.sort_stats('cumulative').print_stats()
1417 # vim:set shiftwidth=3: