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("^([^ <>@]+@[^ ,<>@]+)?$")
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)
76 """Return a pretty-printed XML string for the Element.
78 rough_string = ElementTree.tostring(elem, 'utf-8')
79 reparsed = minidom.parseString(rough_string)
80 return reparsed.toprettyxml(indent=" ")
82 def safe_makedirs(dir):
86 if e.errno == errno.EEXIST:
95 if e.errno == errno.ENOENT:
100 def get_lock(fn, wait=5*60):
103 ends = time.time() + wait
108 fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
112 if time.time() >= ends:
114 sl = min(sl*2, 10, ends - time.time())
120 return Str.translate(string.maketrans("\n\r\t", "$$$"))
122 def DoLink(From, To, File):
124 posix.remove(To + File)
127 posix.link(From + File, To + File)
129 def IsRetired(account):
131 Looks for accountStatus in the LDAP record and tries to
132 match it against one of the known retired statuses
135 status = account['accountStatus']
137 line = status.split()
140 if status == "inactive":
143 elif status == "memorial":
146 elif status == "retiring":
147 # We'll give them a few extra days over what we said
148 age = 6 * 31 * 24 * 60 * 60
150 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
158 #def IsGidDebian(account):
159 # return account['gidNumber'] == 800
161 # See if this user is in the group list
162 def IsInGroup(account, allowed, current_host):
163 # See if the primary group is in the list
164 if str(account['gidNumber']) in allowed: return True
166 # Check the host based ACL
167 if account.is_allowed_by_hostacl(current_host): return True
169 # See if there are supplementary groups
170 if not 'supplementaryGid' in account: return False
173 addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
175 if allowed.has_key(g):
179 def Die(File, F, Fdb):
185 os.remove(File + ".tmp")
189 os.remove(File + ".tdb.tmp")
193 def Done(File, F, Fdb):
196 os.rename(File + ".tmp", File)
199 os.rename(File + ".tdb.tmp", File + ".tdb")
201 # Generate the password list
202 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
205 F = open(File + ".tdb.tmp", "w")
210 # Do not let people try to buffer overflow some busted passwd parser.
211 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
213 userlist[a['uid']] = a['gidNumber']
214 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
220 HomePrefix, a['uid'],
222 line = Sanitize(line) + "\n"
223 F.write("0%u %s" % (i, line))
224 F.write(".%s %s" % (a['uid'], line))
225 F.write("=%d %s" % (a['uidNumber'], line))
228 # Oops, something unspeakable happened.
234 # Return the list of users so we know which keys to export
237 def GenAllUsers(accounts, file):
240 OldMask = os.umask(0022)
241 f = open(file + ".tmp", "w", 0644)
246 all.append( { 'uid': a['uid'],
247 'uidNumber': a['uidNumber'],
248 'active': a.pw_active() and a.shadow_active() } )
251 # Oops, something unspeakable happened.
257 # Generate the shadow list
258 def GenShadow(accounts, File):
261 OldMask = os.umask(0077)
262 F = open(File + ".tdb.tmp", "w", 0600)
267 # If the account is locked, mark it as such in shadow
268 # See Debian Bug #308229 for why we set it to 1 instead of 0
269 if not a.pw_active(): ShadowExpire = '1'
270 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
271 else: ShadowExpire = ''
274 values.append(a['uid'])
275 values.append(a.get_password())
276 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
277 if key in a: values.append(a[key])
278 else: values.append('')
279 values.append(ShadowExpire)
280 line = ':'.join(values)+':'
281 line = Sanitize(line) + "\n"
282 F.write("0%u %s" % (i, line))
283 F.write(".%s %s" % (a['uid'], line))
286 # Oops, something unspeakable happened.
292 # Generate the sudo passwd file
293 def GenShadowSudo(accounts, File, untrusted, current_host):
296 OldMask = os.umask(0077)
297 F = open(File + ".tmp", "w", 0600)
302 if 'sudoPassword' in a:
303 for entry in a['sudoPassword']:
304 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
307 uuid = Match.group(1)
308 status = Match.group(2)
309 hosts = Match.group(3)
310 cryptedpass = Match.group(4)
312 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
314 for_all = hosts == "*"
315 for_this_host = current_host in hosts.split(',')
316 if not (for_all or for_this_host):
318 # ignore * passwords for untrusted hosts, but copy host specific passwords
319 if for_all and untrusted:
322 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
327 Line = "%s:%s" % (a['uid'], Pass)
328 Line = Sanitize(Line) + "\n"
329 F.write("%s" % (Line))
331 # Oops, something unspeakable happened.
337 # Generate the sudo passwd file
338 def GenSSHGitolite(accounts, File):
341 OldMask = os.umask(0022)
342 F = open(File + ".tmp", "w", 0600)
345 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
347 if not 'sshRSAAuthKey' in a: continue
350 prefix = GitoliteSSHRestrictions.replace('@@USER@@', User)
351 for I in a["sshRSAAuthKey"]:
352 if I.startswith('ssh-'):
353 line = "%s %s"%(prefix, I)
355 line = "%s,%s"%(prefix, I)
356 line = Sanitize(line) + "\n"
359 # Oops, something unspeakable happened.
365 # Generate the shadow list
366 def GenSSHShadow(global_dir, accounts):
367 # Fetch all the users
371 if not 'sshRSAAuthKey' in a: continue
374 for I in a['sshRSAAuthKey']:
375 MultipleLine = "%s" % I
376 MultipleLine = Sanitize(MultipleLine)
377 contents.append(MultipleLine)
378 userkeys[a['uid']] = contents
381 # Generate the webPassword list
382 def GenWebPassword(accounts, File):
385 OldMask = os.umask(0077)
386 F = open(File, "w", 0600)
390 if not 'webPassword' in a: continue
391 if not a.pw_active(): continue
393 Pass = str(a['webPassword'])
394 Line = "%s:%s" % (a['uid'], Pass)
395 Line = Sanitize(Line) + "\n"
396 F.write("%s" % (Line))
402 # Generate the voipPassword list
403 def GenVoipPassword(accounts, File):
406 OldMask = os.umask(0077)
407 F = open(File, "w", 0600)
410 root = Element('include')
413 if not 'voipPassword' in a: continue
414 if not a.pw_active(): continue
416 Pass = str(a['voipPassword'])
417 user = Element('user')
418 user.attrib['id'] = "%s" % (a['uid'])
420 params = Element('params')
422 param = Element('param')
424 param.attrib['name'] = "a1-hash"
425 param.attrib['value'] = "%s" % (Pass)
426 variables = Element('variables')
427 user.append(variables)
428 variable = Element('variable')
429 variable.attrib['name'] = "toll_allow"
430 variable.attrib['value'] = "domestic,international,local"
431 variables.append(variable)
433 F.write("%s" % (prettify(root)))
440 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
441 OldMask = os.umask(0077)
442 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
445 if f not in ssh_userkeys:
447 # If we're not exporting their primary group, don't export
450 if userlist[f] in grouprevmap.keys():
451 grname = grouprevmap[userlist[f]]
454 if int(userlist[f]) <= 100:
455 # In these cases, look it up in the normal way so we
456 # deal with cases where, for instance, users are in group
457 # users as their primary group.
458 grname = grp.getgrgid(userlist[f])[0]
463 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])
467 for line in ssh_userkeys[f]:
468 if line.startswith("allowed_hosts=") and ' ' in line:
469 machines, line = line.split('=', 1)[1].split(' ', 1)
470 if current_host not in machines.split(','):
471 continue # skip this key
474 continue # no keys for this host
475 contents = "\n".join(lines) + "\n"
477 to = tarfile.TarInfo(name=f)
478 # These will only be used where the username doesn't
479 # exist on the target system for some reason; hence,
480 # in those cases, the safest thing is for the file to
481 # be owned by root but group nobody. This deals with
482 # the bloody obscure case where the group fails to exist
483 # whilst the user does (in which case we want to avoid
484 # ending up with a file which is owned user:root to avoid
485 # a fairly obvious attack vector)
488 # Using the username / groupname fields avoids any need
489 # to give a shit^W^W^Wcare about the UIDoffset stuff.
493 to.mtime = int(time.time())
494 to.size = len(contents)
496 tf.addfile(to, StringIO(contents))
499 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
501 # add a list of groups to existing groups,
502 # including all subgroups thereof, recursively.
503 # basically this proceduces the transitive hull of the groups in
505 def addGroups(existingGroups, newGroups, uid, current_host):
506 for group in newGroups:
507 # if it's a <group>@host, split it and verify it's on the current host.
508 s = group.split('@', 1)
509 if len(s) == 2 and s[1] != current_host:
513 # let's see if we handled this group already
514 if group in existingGroups:
517 if not GroupIDMap.has_key(group):
518 print "Group", group, "does not exist but", uid, "is in it"
521 existingGroups.append(group)
523 if SubGroupMap.has_key(group):
524 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
526 # Generate the group list
527 def GenGroup(accounts, File, current_host):
531 F = open(File + ".tdb.tmp", "w")
533 # Generate the GroupMap
537 GroupHasPrimaryMembers = {}
539 # Sort them into a list of groups having a set of users
541 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
542 if not 'supplementaryGid' in a: continue
545 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
547 GroupMap[g].append(a['uid'])
549 # Output the group file.
551 for x in GroupMap.keys():
552 if not x in GroupIDMap:
555 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
558 grouprevmap[GroupIDMap[x]] = x
560 Line = "%s:x:%u:" % (x, GroupIDMap[x])
562 for I in GroupMap[x]:
563 Line = Line + ("%s%s" % (Comma, I))
565 Line = Sanitize(Line) + "\n"
566 F.write("0%u %s" % (J, Line))
567 F.write(".%s %s" % (x, Line))
568 F.write("=%u %s" % (GroupIDMap[x], Line))
571 # Oops, something unspeakable happened.
579 def CheckForward(accounts):
581 if not 'emailForward' in a: continue
585 # Do not allow people to try to buffer overflow busted parsers
586 if len(a['emailForward']) > 200: delete = True
587 # Check the forwarding address
588 elif EmailCheck.match(a['emailForward']) is None: delete = True
591 a.delete_mailforward()
593 # Generate the email forwarding list
594 def GenForward(accounts, File):
597 OldMask = os.umask(0022)
598 F = open(File + ".tmp", "w", 0644)
602 if not 'emailForward' in a: continue
603 Line = "%s: %s" % (a['uid'], a['emailForward'])
604 Line = Sanitize(Line) + "\n"
607 # Oops, something unspeakable happened.
613 def GenCDB(accounts, File, key):
616 OldMask = os.umask(0022)
617 # nothing else does the fsync stuff, so why do it here?
618 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
619 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
622 # Write out the email address for each user
624 if not key in a: continue
627 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
630 # Oops, something unspeakable happened.
634 if Fdb.close() != None:
635 raise "cdbmake gave an error"
637 # Generate the anon XEarth marker file
638 def GenMarkers(accounts, File):
641 F = open(File + ".tmp", "w")
643 # Write out the position for each user
645 if not ('latitude' in a and 'longitude' in a): continue
647 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
648 Line = Sanitize(Line) + "\n"
653 # Oops, something unspeakable happened.
659 # Generate the debian-private subscription list
660 def GenPrivate(accounts, File):
663 F = open(File + ".tmp", "w")
665 # Write out the position for each user
667 if not a.is_active_user(): continue
668 if not 'privateSub' in a: continue
670 Line = "%s"%(a['privateSub'])
671 Line = Sanitize(Line) + "\n"
676 # Oops, something unspeakable happened.
682 # Generate a list of locked accounts
683 def GenDisabledAccounts(accounts, File):
686 F = open(File + ".tmp", "w")
687 disabled_accounts = []
689 # Fetch all the users
691 if a.pw_active(): continue
692 Line = "%s:%s" % (a['uid'], "Account is locked")
693 disabled_accounts.append(a)
694 F.write(Sanitize(Line) + "\n")
696 # Oops, something unspeakable happened.
701 return disabled_accounts
703 # Generate the list of local addresses that refuse all mail
704 def GenMailDisable(accounts, File):
707 F = open(File + ".tmp", "w")
710 if not 'mailDisableMessage' in a: continue
711 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
712 Line = Sanitize(Line) + "\n"
715 # Oops, something unspeakable happened.
721 # Generate a list of uids that should have boolean affects applied
722 def GenMailBool(accounts, File, key):
725 F = open(File + ".tmp", "w")
728 if not key in a: continue
729 if not a[key] == 'TRUE': continue
730 Line = "%s"%(a['uid'])
731 Line = Sanitize(Line) + "\n"
734 # Oops, something unspeakable happened.
740 # Generate a list of hosts for RBL or whitelist purposes.
741 def GenMailList(accounts, File, key):
744 F = open(File + ".tmp", "w")
746 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
747 else: validregex = re.compile('^[-\w.]+$')
750 if not key in a: continue
752 filtered = filter(lambda z: validregex.match(z), a[key])
753 if len(filtered) == 0: continue
754 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
755 line = a['uid'] + ': ' + ' : '.join(filtered)
756 line = Sanitize(line) + "\n"
759 # Oops, something unspeakable happened.
765 def isRoleAccount(account):
766 return 'debianRoleAccount' in account['objectClass']
768 # Generate the DNS Zone file
769 def GenDNS(accounts, File):
772 F = open(File + ".tmp", "w")
774 # Fetch all the users
777 # Write out the zone file entry for each user
779 if not 'dnsZoneEntry' in a: continue
780 if not a.is_active_user() and not isRoleAccount(a): continue
783 F.write("; %s\n"%(a.email_address()))
784 for z in a["dnsZoneEntry"]:
785 Split = z.lower().split()
786 if Split[1].lower() == 'in':
787 Line = " ".join(Split) + "\n"
790 Host = Split[0] + DNSZone
791 if BSMTPCheck.match(Line) != None:
792 F.write("; Has BSMTP\n")
794 # Write some identification information
795 if not RRs.has_key(Host):
796 if Split[2].lower() in ["a", "aaaa"]:
797 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
798 for y in a["keyFingerPrint"]:
799 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
803 Line = "; Err %s"%(str(Split))
808 F.write("; Errors:\n")
809 for line in str(e).split("\n"):
810 F.write("; %s\n"%(line))
813 # Oops, something unspeakable happened.
819 def ExtractDNSInfo(x):
823 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
826 if x[1].has_key("ipHostNumber"):
827 for I in x[1]["ipHostNumber"]:
828 if IsV6Addr.match(I) != None:
829 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
831 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
835 if 'sshRSAHostKey' in x[1]:
836 for I in x[1]["sshRSAHostKey"]:
838 if Split[0] == 'ssh-rsa':
840 if Split[0] == 'ssh-dss':
842 if Algorithm == None:
844 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
845 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
847 if 'architecture' in x[1]:
848 Arch = GetAttr(x, "architecture")
850 if x[1].has_key("machine"):
851 Mach = " " + GetAttr(x, "machine")
852 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
854 if x[1].has_key("mXRecord"):
855 for I in x[1]["mXRecord"]:
856 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
860 # Generate the DNS records
861 def GenZoneRecords(host_attrs, File):
864 F = open(File + ".tmp", "w")
866 # Fetch all the hosts
868 if x[1].has_key("hostname") == 0:
871 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
874 DNSInfo = ExtractDNSInfo(x)
878 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
881 Line = "\t\t\t%s" % (Line)
885 # this would write sshfp lines for services on machines
886 # but we can't yet, since some are cnames and we'll make
887 # an invalid zonefile
889 # for i in x[1].get("purpose", []):
890 # m = PurposeHostField.match(i)
893 # # we ignore [[*..]] entries
894 # if m.startswith('*'):
896 # if m.startswith('-'):
899 # if not m.endswith(HostDomain):
901 # if not m.endswith('.'):
903 # for Line in DNSInfo:
904 # if isSSHFP.match(Line):
905 # Line = "%s\t%s" % (m, Line)
906 # F.write(Line + "\n")
908 # Oops, something unspeakable happened.
914 # Generate the BSMTP file
915 def GenBSMTP(accounts, File, HomePrefix):
918 F = open(File + ".tmp", "w")
920 # Write out the zone file entry for each user
922 if not 'dnsZoneEntry' in a: continue
923 if not a.is_active_user(): continue
926 for z in a["dnsZoneEntry"]:
927 Split = z.lower().split()
928 if Split[1].lower() == 'in':
929 for y in range(0, len(Split)):
932 Line = " ".join(Split) + "\n"
934 Host = Split[0] + DNSZone
935 if BSMTPCheck.match(Line) != None:
936 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
937 a['uid'], HomePrefix, a['uid'], Host))
940 F.write("; Errors\n")
943 # Oops, something unspeakable happened.
949 def HostToIP(Host, mapped=True):
953 if Host[1].has_key("ipHostNumber"):
954 for addr in Host[1]["ipHostNumber"]:
955 IPAdresses.append(addr)
956 if IsV6Addr.match(addr) is None and mapped == "True":
957 IPAdresses.append("::ffff:"+addr)
961 # Generate the ssh known hosts file
962 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
965 OldMask = os.umask(0022)
966 F = open(File + ".tmp", "w", 0644)
970 if x[1].has_key("hostname") == 0 or \
971 x[1].has_key("sshRSAHostKey") == 0:
973 Host = GetAttr(x, "hostname")
975 if Host.endswith(HostDomain):
976 HostNames.append(Host[:-(len(HostDomain) + 1)])
978 # in the purpose field [[host|some other text]] (where some other text is optional)
979 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
980 # file. But so that we don't have to add everything we link we can add an asterisk
981 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
982 # http linking it we also support [[-hostname]] entries.
983 for i in x[1].get("purpose", []):
984 m = PurposeHostField.match(i)
987 # we ignore [[*..]] entries
988 if m.startswith('*'):
990 if m.startswith('-'):
994 if m.endswith(HostDomain):
995 HostNames.append(m[:-(len(HostDomain) + 1)])
997 for I in x[1]["sshRSAHostKey"]:
998 if mode and mode == 'authorized_keys':
1000 if 'sshdistAuthKeysHost' in x[1]:
1001 hosts += x[1]['sshdistAuthKeysHost']
1002 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1003 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1004 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1006 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1007 Line = Sanitize(Line) + "\n"
1009 # Oops, something unspeakable happened.
1015 # Generate the debianhosts file (list of all IP addresses)
1016 def GenHosts(host_attrs, File):
1019 OldMask = os.umask(0022)
1020 F = open(File + ".tmp", "w", 0644)
1025 for x in host_attrs:
1027 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1030 if not 'ipHostNumber' in x[1]:
1033 addrs = x[1]["ipHostNumber"]
1035 if addr not in seen:
1037 addr = Sanitize(addr) + "\n"
1040 # Oops, something unspeakable happened.
1046 def replaceTree(src, dst_basedir):
1047 bn = os.path.basename(src)
1048 dst = os.path.join(dst_basedir, bn)
1050 shutil.copytree(src, dst)
1052 def GenKeyrings(OutDir):
1054 if os.path.isdir(k):
1055 replaceTree(k, OutDir)
1057 shutil.copy(k, OutDir)
1060 def get_accounts(ldap_conn):
1061 # Fetch all the users
1062 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1063 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1064 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1065 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1066 "shadowExpire", "emailForward", "latitude", "longitude",\
1067 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1068 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1069 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1070 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1071 "mailContentInspectionAction", "webPassword", "voipPassword"])
1073 if passwd_attrs is None:
1074 raise UDEmptyList, "No Users"
1075 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1076 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1080 def get_hosts(ldap_conn):
1081 # Fetch all the hosts
1082 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1083 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1084 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1086 if HostAttrs == None:
1087 raise UDEmptyList, "No Hosts"
1089 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1094 def make_ldap_conn():
1095 # Connect to the ldap server
1097 # for testing purposes it's sometimes useful to pass username/password
1098 # via the environment
1099 if 'UD_CREDENTIALS' in os.environ:
1100 Pass = os.environ['UD_CREDENTIALS'].split()
1102 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1103 Pass = F.readline().strip().split(" ")
1105 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1111 def setup_group_maps(l):
1112 # Fetch all the groups
1115 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1116 ["gid", "gidNumber", "subGroup"])
1118 # Generate the subgroup_map and group_id_map
1120 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1122 if x[1].has_key("gidNumber") == 0:
1124 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1125 if x[1].has_key("subGroup") != 0:
1126 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1130 SubGroupMap = subgroup_map
1131 GroupIDMap = group_id_map
1133 def generate_all(global_dir, ldap_conn):
1134 accounts = get_accounts(ldap_conn)
1135 host_attrs = get_hosts(ldap_conn)
1138 # Generate global things
1139 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1141 accounts = filter(lambda x: not IsRetired(x), accounts)
1142 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1144 CheckForward(accounts)
1146 GenMailDisable(accounts, global_dir + "mail-disable")
1147 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1148 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1149 GenPrivate(accounts, global_dir + "debian-private")
1150 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1151 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1152 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1153 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1154 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1155 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1156 GenWebPassword(accounts, global_dir + "web-passwords")
1157 GenVoipPassword(accounts, global_dir + "voip-passwords")
1158 GenKeyrings(global_dir)
1161 GenForward(accounts, global_dir + "forward-alias")
1163 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1164 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1166 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1167 GenMarkers(accounts, global_dir + "markers")
1168 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1169 GenHosts(host_attrs, global_dir + "debianhosts")
1170 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1172 GenDNS(accounts, global_dir + "dns-zone")
1173 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1175 setup_group_maps(ldap_conn)
1177 for host in host_attrs:
1178 if not "hostname" in host[1]:
1180 generate_host(host, global_dir, accounts, ssh_userkeys)
1182 def generate_host(host, global_dir, accounts, ssh_userkeys):
1183 current_host = host[1]['hostname'][0]
1184 OutDir = global_dir + current_host + '/'
1185 if not os.path.isdir(OutDir):
1188 # Get the group list and convert any named groups to numerics
1190 for groupname in AllowedGroupsPreload.strip().split(" "):
1191 GroupList[groupname] = True
1192 if 'allowedGroups' in host[1]:
1193 for groupname in host[1]['allowedGroups']:
1194 GroupList[groupname] = True
1195 for groupname in GroupList.keys():
1196 if groupname in GroupIDMap:
1197 GroupList[str(GroupIDMap[groupname])] = True
1200 if 'exportOptions' in host[1]:
1201 for extra in host[1]['exportOptions']:
1202 ExtraList[extra.upper()] = True
1205 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), accounts)
1207 DoLink(global_dir, OutDir, "debianhosts")
1208 DoLink(global_dir, OutDir, "ssh_known_hosts")
1209 DoLink(global_dir, OutDir, "disabled-accounts")
1212 if 'NOPASSWD' in ExtraList:
1213 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1215 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1217 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1218 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1220 # Now we know who we're allowing on the machine, export
1221 # the relevant ssh keys
1222 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1224 if not 'NOPASSWD' in ExtraList:
1225 GenShadow(accounts, OutDir + "shadow")
1227 # Link in global things
1228 if not 'NOMARKERS' in ExtraList:
1229 DoLink(global_dir, OutDir, "markers")
1230 DoLink(global_dir, OutDir, "mail-forward.cdb")
1231 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1232 DoLink(global_dir, OutDir, "mail-disable")
1233 DoLink(global_dir, OutDir, "mail-greylist")
1234 DoLink(global_dir, OutDir, "mail-callout")
1235 DoLink(global_dir, OutDir, "mail-rbl")
1236 DoLink(global_dir, OutDir, "mail-rhsbl")
1237 DoLink(global_dir, OutDir, "mail-whitelist")
1238 DoLink(global_dir, OutDir, "all-accounts.json")
1239 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1240 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1241 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1244 DoLink(global_dir, OutDir, "forward-alias")
1246 if 'DNS' in ExtraList:
1247 DoLink(global_dir, OutDir, "dns-zone")
1248 DoLink(global_dir, OutDir, "dns-sshfp")
1250 if 'AUTHKEYS' in ExtraList:
1251 DoLink(global_dir, OutDir, "authorized_keys")
1253 if 'BSMTP' in ExtraList:
1254 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1256 if 'PRIVATE' in ExtraList:
1257 DoLink(global_dir, OutDir, "debian-private")
1259 if 'GITOLITE' in ExtraList:
1260 DoLink(global_dir, OutDir, "ssh-gitolite")
1262 if 'WEB-PASSWORDS' in ExtraList:
1263 DoLink(global_dir, OutDir, "web-passwords")
1265 if 'VOIP-PASSWORDS' in ExtraList:
1266 DoLink(global_dir, OutDir, "voip-passwords")
1268 if 'KEYRING' in ExtraList:
1270 bn = os.path.basename(k)
1271 if os.path.isdir(k):
1272 src = os.path.join(global_dir, bn)
1273 replaceTree(src, OutDir)
1275 DoLink(global_dir, OutDir, bn)
1279 bn = os.path.basename(k)
1280 target = os.path.join(OutDir, bn)
1281 if os.path.isdir(target):
1284 posix.remove(target)
1287 DoLink(global_dir, OutDir, "last_update.trace")
1290 def getLastLDAPChangeTime(l):
1291 mods = l.search_s('cn=log',
1292 ldap.SCOPE_ONELEVEL,
1293 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1298 # Sort the list by reqEnd
1299 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1300 # Take the last element in the array
1301 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1305 def getLastKeyringChangeTime():
1308 mt = os.path.getmtime(k)
1314 def getLastBuildTime(gdir):
1315 cache_last_ldap_mod = 0
1316 cache_last_unix_mod = 0
1319 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1320 cache_last_mod=fd.read().split()
1322 cache_last_ldap_mod = cache_last_mod[0]
1323 cache_last_unix_mod = int(cache_last_mod[1])
1324 except IndexError, ValueError:
1328 if e.errno == errno.ENOENT:
1333 return (cache_last_ldap_mod, cache_last_unix_mod)
1336 parser = optparse.OptionParser()
1337 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1338 help="Output directory.")
1339 parser.add_option("-f", "--force", dest="force", action="store_true",
1340 help="Force generation, even if not update to LDAP has happened.")
1342 (options, args) = parser.parse_args()
1347 if options.generatedir is not None:
1348 generate_dir = os.environ['UD_GENERATEDIR']
1349 elif 'UD_GENERATEDIR' in os.environ:
1350 generate_dir = os.environ['UD_GENERATEDIR']
1352 generate_dir = GenerateDir
1355 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1356 lock = get_lock( lockf )
1358 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1361 l = make_ldap_conn()
1363 time_started = int(time.time())
1364 ldap_last_mod = getLastLDAPChangeTime(l)
1365 unix_last_mod = getLastKeyringChangeTime()
1366 cache_last_ldap_mod, cache_last_unix_mod = getLastBuildTime(generate_dir)
1368 need_update = (ldap_last_mod > cache_last_ldap_mod) or (unix_last_mod > cache_last_unix_mod)
1370 if not options.force and not need_update:
1371 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1372 fd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1376 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1377 generate_all(generate_dir, l)
1378 tracefd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1382 if __name__ == "__main__":
1383 if 'UD_PROFILE' in os.environ:
1386 cProfile.run('ud_generate()', "udg_prof")
1387 p = pstats.Stats('udg_prof')
1388 ##p.sort_stats('time').print_stats()
1389 p.sort_stats('cumulative').print_stats()
1395 # vim:set shiftwidth=3: