3 # Generates passwd, shadow and group files from the ldap directory.
5 # Copyright (c) 2000-2001 Jason Gunthorpe <jgg@debian.org>
6 # Copyright (c) 2003-2004 James Troup <troup@debian.org>
7 # Copyright (c) 2004-2005,7 Joey Schulze <joey@infodrom.org>
8 # Copyright (c) 2001-2007 Ryan Murray <rmurray@debian.org>
9 # Copyright (c) 2008,2009,2010,2011 Peter Palfrader <peter@palfrader.org>
10 # Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
11 # Copyright (c) 2008 Mark Hymers <mhy@debian.org>
12 # Copyright (c) 2008 Luk Claes <luk@debian.org>
13 # Copyright (c) 2008 Thomas Viehmann <tv@beamnet.de>
14 # Copyright (c) 2009 Stephen Gran <steve@lobefin.net>
15 # Copyright (c) 2010 Helmut Grohne <helmut@subdivi.de>
17 # This program is free software; you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation; either version 2 of the License, or
20 # (at your option) any later version.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
31 import string, re, time, ldap, optparse, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp, fcntl, dbm
32 from userdir_ldap import *
33 from userdir_exceptions import *
35 from xml.etree.ElementTree import Element, SubElement, Comment
36 from xml.etree import ElementTree
37 from xml.dom import minidom
39 from cStringIO import StringIO
41 from StringIO import StringIO
43 import simplejson as json
46 if not '__author__' in json.__dict__:
47 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
48 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
51 sys.stderr.write("You should probably not run ud-generate as root.\n")
63 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
66 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)(,\s*([^ <>@]+@[^ ,<>@]+))*$")
67 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
68 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
69 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
70 isSSHFP = re.compile("^\s*IN\s+SSHFP")
71 DNSZone = ".debian.net"
72 Keyrings = ConfModule.sync_keyrings.split(":")
73 GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
74 GitoliteExportHosts = re.compile(getattr(ConfModule, "gitoliteexporthosts", "."))
75 MX_remap = json.loads(ConfModule.MX_remap)
78 """Return a pretty-printed XML string for the Element.
80 rough_string = ElementTree.tostring(elem, 'utf-8')
81 reparsed = minidom.parseString(rough_string)
82 return reparsed.toprettyxml(indent=" ")
84 def safe_makedirs(dir):
88 if e.errno == errno.EEXIST:
97 if e.errno == errno.ENOENT:
102 def get_lock(fn, wait=5*60):
105 ends = time.time() + wait
110 fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
114 if time.time() >= ends:
116 sl = min(sl*2, 10, ends - time.time())
122 return Str.translate(string.maketrans("\n\r\t", "$$$"))
124 def DoLink(From, To, File):
126 posix.remove(To + File)
129 posix.link(From + File, To + File)
131 def IsRetired(account):
133 Looks for accountStatus in the LDAP record and tries to
134 match it against one of the known retired statuses
137 status = account['accountStatus']
139 line = status.split()
142 if status == "inactive":
145 elif status == "memorial":
148 elif status == "retiring":
149 # We'll give them a few extra days over what we said
150 age = 6 * 31 * 24 * 60 * 60
152 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
160 #def IsGidDebian(account):
161 # return account['gidNumber'] == 800
163 # See if this user is in the group list
164 def IsInGroup(account, allowed, current_host):
165 # See if the primary group is in the list
166 if str(account['gidNumber']) in allowed: return True
168 # Check the host based ACL
169 if account.is_allowed_by_hostacl(current_host): return True
171 # See if there are supplementary groups
172 if not 'supplementaryGid' in account: return False
175 addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
181 def Die(File, F, Fdb):
187 os.remove(File + ".tmp")
191 os.remove(File + ".tdb.tmp")
195 def Done(File, F, Fdb):
198 os.rename(File + ".tmp", File)
201 os.rename(File + ".tdb.tmp", File + ".tdb")
203 # Generate the password list
204 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
207 F = open(File + ".tdb.tmp", "w")
212 # Do not let people try to buffer overflow some busted passwd parser.
213 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
215 userlist[a['uid']] = a['gidNumber']
216 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
222 HomePrefix, a['uid'],
224 line = Sanitize(line) + "\n"
225 F.write("0%u %s" % (i, line))
226 F.write(".%s %s" % (a['uid'], line))
227 F.write("=%d %s" % (a['uidNumber'], line))
230 # Oops, something unspeakable happened.
236 # Return the list of users so we know which keys to export
239 def GenAllUsers(accounts, file):
242 OldMask = os.umask(0022)
243 f = open(file + ".tmp", "w", 0644)
248 all.append( { 'uid': a['uid'],
249 'uidNumber': a['uidNumber'],
250 'active': a.pw_active() and a.shadow_active() } )
253 # Oops, something unspeakable happened.
259 # Generate the shadow list
260 def GenShadow(accounts, File):
263 OldMask = os.umask(0077)
264 F = open(File + ".tdb.tmp", "w", 0600)
269 # If the account is locked, mark it as such in shadow
270 # See Debian Bug #308229 for why we set it to 1 instead of 0
271 if not a.pw_active(): ShadowExpire = '1'
272 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
273 else: ShadowExpire = ''
276 values.append(a['uid'])
277 values.append(a.get_password())
278 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
279 if key in a: values.append(a[key])
280 else: values.append('')
281 values.append(ShadowExpire)
282 line = ':'.join(values)+':'
283 line = Sanitize(line) + "\n"
284 F.write("0%u %s" % (i, line))
285 F.write(".%s %s" % (a['uid'], line))
288 # Oops, something unspeakable happened.
294 # Generate the sudo passwd file
295 def GenShadowSudo(accounts, File, untrusted, current_host):
298 OldMask = os.umask(0077)
299 F = open(File + ".tmp", "w", 0600)
304 if 'sudoPassword' in a:
305 for entry in a['sudoPassword']:
306 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
309 uuid = Match.group(1)
310 status = Match.group(2)
311 hosts = Match.group(3)
312 cryptedpass = Match.group(4)
314 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
316 for_all = hosts == "*"
317 for_this_host = current_host in hosts.split(',')
318 if not (for_all or for_this_host):
320 # ignore * passwords for untrusted hosts, but copy host specific passwords
321 if for_all and untrusted:
324 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
329 Line = "%s:%s" % (a['uid'], Pass)
330 Line = Sanitize(Line) + "\n"
331 F.write("%s" % (Line))
333 # Oops, something unspeakable happened.
339 # Generate the sudo passwd file
340 def GenSSHGitolite(accounts, hosts, File):
343 OldMask = os.umask(0022)
344 F = open(File + ".tmp", "w", 0600)
347 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
349 if not 'sshRSAAuthKey' in a: continue
352 prefix = GitoliteSSHRestrictions.replace('@@USER@@', User)
353 for I in a["sshRSAAuthKey"]:
354 if I.startswith('ssh-'):
355 line = "%s %s"%(prefix, I)
357 line = "%s,%s"%(prefix, I)
358 line = Sanitize(line) + "\n"
361 for dn, attrs in hosts:
362 if not 'sshRSAHostKey' in attrs: continue
363 hostname = "host-" + attrs['hostname'][0]
364 prefix = GitoliteSSHRestrictions.replace('@@USER@@', hostname)
365 for I in attrs["sshRSAHostKey"]:
366 line = "%s %s"%(prefix, I)
367 line = Sanitize(line) + "\n"
370 # Oops, something unspeakable happened.
376 # Generate the shadow list
377 def GenSSHShadow(global_dir, accounts):
378 # Fetch all the users
382 if not 'sshRSAAuthKey' in a: continue
385 for I in a['sshRSAAuthKey']:
386 MultipleLine = "%s" % I
387 MultipleLine = Sanitize(MultipleLine)
388 contents.append(MultipleLine)
389 userkeys[a['uid']] = contents
392 # Generate the webPassword list
393 def GenWebPassword(accounts, File):
396 OldMask = os.umask(0077)
397 F = open(File, "w", 0600)
401 if not 'webPassword' in a: continue
402 if not a.pw_active(): continue
404 Pass = str(a['webPassword'])
405 Line = "%s:%s" % (a['uid'], Pass)
406 Line = Sanitize(Line) + "\n"
407 F.write("%s" % (Line))
413 # Generate the voipPassword list
414 def GenVoipPassword(accounts, File):
417 OldMask = os.umask(0077)
418 F = open(File, "w", 0600)
422 if not 'voipPassword' in a: continue
423 if not a.pw_active(): continue
425 Line = "%s@debian.org:%s:sip.debian.org:AUTHORIZED" % (a['uid'], str(a['voipPassword']))
426 Line = Sanitize(Line) + "\n"
427 F.write("%s" % (Line))
433 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
434 OldMask = os.umask(0077)
435 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
438 if f not in ssh_userkeys:
440 # If we're not exporting their primary group, don't export
443 if userlist[f] in grouprevmap.keys():
444 grname = grouprevmap[userlist[f]]
447 if int(userlist[f]) <= 100:
448 # In these cases, look it up in the normal way so we
449 # deal with cases where, for instance, users are in group
450 # users as their primary group.
451 grname = grp.getgrgid(userlist[f])[0]
456 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])
460 for line in ssh_userkeys[f]:
461 if line.startswith("allowed_hosts=") and ' ' in line:
462 machines, line = line.split('=', 1)[1].split(' ', 1)
463 if current_host not in machines.split(','):
464 continue # skip this key
467 continue # no keys for this host
468 contents = "\n".join(lines) + "\n"
470 to = tarfile.TarInfo(name=f)
471 # These will only be used where the username doesn't
472 # exist on the target system for some reason; hence,
473 # in those cases, the safest thing is for the file to
474 # be owned by root but group nobody. This deals with
475 # the bloody obscure case where the group fails to exist
476 # whilst the user does (in which case we want to avoid
477 # ending up with a file which is owned user:root to avoid
478 # a fairly obvious attack vector)
481 # Using the username / groupname fields avoids any need
482 # to give a shit^W^W^Wcare about the UIDoffset stuff.
486 to.mtime = int(time.time())
487 to.size = len(contents)
489 tf.addfile(to, StringIO(contents))
492 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
494 # add a list of groups to existing groups,
495 # including all subgroups thereof, recursively.
496 # basically this proceduces the transitive hull of the groups in
498 def addGroups(existingGroups, newGroups, uid, current_host):
499 for group in newGroups:
500 # if it's a <group>@host, split it and verify it's on the current host.
501 s = group.split('@', 1)
502 if len(s) == 2 and s[1] != current_host:
506 # let's see if we handled this group already
507 if group in existingGroups:
510 if not GroupIDMap.has_key(group):
511 print "Group", group, "does not exist but", uid, "is in it"
514 existingGroups.append(group)
516 if SubGroupMap.has_key(group):
517 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
519 # Generate the group list
520 def GenGroup(accounts, File, current_host):
524 F = open(File + ".tdb.tmp", "w")
526 # Generate the GroupMap
530 GroupHasPrimaryMembers = {}
532 # Sort them into a list of groups having a set of users
534 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
535 if not 'supplementaryGid' in a: continue
538 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
540 GroupMap[g].append(a['uid'])
542 # Output the group file.
544 for x in GroupMap.keys():
545 if not x in GroupIDMap:
548 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
551 grouprevmap[GroupIDMap[x]] = x
553 Line = "%s:x:%u:" % (x, GroupIDMap[x])
555 for I in GroupMap[x]:
556 Line = Line + ("%s%s" % (Comma, I))
558 Line = Sanitize(Line) + "\n"
559 F.write("0%u %s" % (J, Line))
560 F.write(".%s %s" % (x, Line))
561 F.write("=%u %s" % (GroupIDMap[x], Line))
564 # Oops, something unspeakable happened.
572 def CheckForward(accounts):
574 if not 'emailForward' in a: continue
578 # Do not allow people to try to buffer overflow busted parsers
579 if len(a['emailForward']) > 200: delete = True
580 # Check the forwarding address
581 elif EmailCheck.match(a['emailForward']) is None: delete = True
584 a.delete_mailforward()
586 # Generate the email forwarding list
587 def GenForward(accounts, File):
590 OldMask = os.umask(0022)
591 F = open(File + ".tmp", "w", 0644)
595 if not 'emailForward' in a: continue
596 Line = "%s: %s" % (a['uid'], a['emailForward'])
597 Line = Sanitize(Line) + "\n"
600 # Oops, something unspeakable happened.
606 def GenCDB(accounts, File, key):
609 OldMask = os.umask(0022)
610 # nothing else does the fsync stuff, so why do it here?
611 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
612 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
615 # Write out the email address for each user
617 if not key in a: continue
620 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
623 # Oops, something unspeakable happened.
627 if Fdb.close() != None:
628 raise "cdbmake gave an error"
630 def GenDBM(accounts, File, key):
632 OldMask = os.umask(0022)
633 fn = os.path.join(File).encode('ascii', 'ignore')
640 Fdb = dbm.open(fn, "c")
643 # Write out the email address for each user
645 if not key in a: continue
652 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
653 os.remove(File + ".db")
655 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
656 os.rename (File + ".db", File)
658 # Generate the anon XEarth marker file
659 def GenMarkers(accounts, File):
662 F = open(File + ".tmp", "w")
664 # Write out the position for each user
666 if not ('latitude' in a and 'longitude' in a): continue
668 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
669 Line = Sanitize(Line) + "\n"
674 # Oops, something unspeakable happened.
680 # Generate the debian-private subscription list
681 def GenPrivate(accounts, File):
684 F = open(File + ".tmp", "w")
686 # Write out the position for each user
688 if not a.is_active_user(): continue
689 if a.is_guest_account(): continue
690 if not 'privateSub' in a: continue
692 Line = "%s"%(a['privateSub'])
693 Line = Sanitize(Line) + "\n"
698 # Oops, something unspeakable happened.
704 # Generate a list of locked accounts
705 def GenDisabledAccounts(accounts, File):
708 F = open(File + ".tmp", "w")
709 disabled_accounts = []
711 # Fetch all the users
713 if a.pw_active(): continue
714 Line = "%s:%s" % (a['uid'], "Account is locked")
715 disabled_accounts.append(a)
716 F.write(Sanitize(Line) + "\n")
718 # Oops, something unspeakable happened.
723 return disabled_accounts
725 # Generate the list of local addresses that refuse all mail
726 def GenMailDisable(accounts, File):
729 F = open(File + ".tmp", "w")
732 if not 'mailDisableMessage' in a: continue
733 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
734 Line = Sanitize(Line) + "\n"
737 # Oops, something unspeakable happened.
743 # Generate a list of uids that should have boolean affects applied
744 def GenMailBool(accounts, File, key):
747 F = open(File + ".tmp", "w")
750 if not key in a: continue
751 if not a[key] == 'TRUE': continue
752 Line = "%s"%(a['uid'])
753 Line = Sanitize(Line) + "\n"
756 # Oops, something unspeakable happened.
762 # Generate a list of hosts for RBL or whitelist purposes.
763 def GenMailList(accounts, File, key):
766 F = open(File + ".tmp", "w")
768 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
769 else: validregex = re.compile('^[-\w.]+$')
772 if not key in a: continue
774 filtered = filter(lambda z: validregex.match(z), a[key])
775 if len(filtered) == 0: continue
776 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
777 line = a['uid'] + ': ' + ' : '.join(filtered)
778 line = Sanitize(line) + "\n"
781 # Oops, something unspeakable happened.
787 def isRoleAccount(account):
788 return 'debianRoleAccount' in account['objectClass']
790 # Generate the DNS Zone file
791 def GenDNS(accounts, File):
794 F = open(File + ".tmp", "w")
796 # Fetch all the users
799 # Write out the zone file entry for each user
801 if not 'dnsZoneEntry' in a: continue
802 if not a.is_active_user() and not isRoleAccount(a): continue
803 if a.is_guest_account(): continue
806 F.write("; %s\n"%(a.email_address()))
807 for z in a["dnsZoneEntry"]:
808 Split = z.lower().split()
809 if Split[1].lower() == 'in':
810 Line = " ".join(Split) + "\n"
813 Host = Split[0] + DNSZone
814 if BSMTPCheck.match(Line) != None:
815 F.write("; Has BSMTP\n")
817 # Write some identification information
818 if not RRs.has_key(Host):
819 if Split[2].lower() in ["a", "aaaa"]:
820 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
821 for y in a["keyFingerPrint"]:
822 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
826 Line = "; Err %s"%(str(Split))
831 F.write("; Errors:\n")
832 for line in str(e).split("\n"):
833 F.write("; %s\n"%(line))
836 # Oops, something unspeakable happened.
844 socket.inet_pton(socket.AF_INET6, i)
849 def ExtractDNSInfo(x):
853 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
856 if x[1].has_key("ipHostNumber"):
857 for I in x[1]["ipHostNumber"]:
859 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
861 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
865 if 'sshRSAHostKey' in x[1]:
866 for I in x[1]["sshRSAHostKey"]:
868 if Split[0] == 'ssh-rsa':
870 if Split[0] == 'ssh-dss':
872 if Algorithm == None:
874 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
875 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
877 if 'architecture' in x[1]:
878 Arch = GetAttr(x, "architecture")
880 if x[1].has_key("machine"):
881 Mach = " " + GetAttr(x, "machine")
882 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
884 if x[1].has_key("mXRecord"):
885 for I in x[1]["mXRecord"]:
887 for e in MX_remap[I]:
888 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
890 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
894 # Generate the DNS records
895 def GenZoneRecords(host_attrs, File):
898 F = open(File + ".tmp", "w")
900 # Fetch all the hosts
902 if x[1].has_key("hostname") == 0:
905 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
908 DNSInfo = ExtractDNSInfo(x)
912 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
915 Line = "\t\t\t%s" % (Line)
919 # this would write sshfp lines for services on machines
920 # but we can't yet, since some are cnames and we'll make
921 # an invalid zonefile
923 # for i in x[1].get("purpose", []):
924 # m = PurposeHostField.match(i)
927 # # we ignore [[*..]] entries
928 # if m.startswith('*'):
930 # if m.startswith('-'):
933 # if not m.endswith(HostDomain):
935 # if not m.endswith('.'):
937 # for Line in DNSInfo:
938 # if isSSHFP.match(Line):
939 # Line = "%s\t%s" % (m, Line)
940 # F.write(Line + "\n")
942 # Oops, something unspeakable happened.
948 # Generate the BSMTP file
949 def GenBSMTP(accounts, File, HomePrefix):
952 F = open(File + ".tmp", "w")
954 # Write out the zone file entry for each user
956 if not 'dnsZoneEntry' in a: continue
957 if not a.is_active_user(): continue
960 for z in a["dnsZoneEntry"]:
961 Split = z.lower().split()
962 if Split[1].lower() == 'in':
963 for y in range(0, len(Split)):
966 Line = " ".join(Split) + "\n"
968 Host = Split[0] + DNSZone
969 if BSMTPCheck.match(Line) != None:
970 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
971 a['uid'], HomePrefix, a['uid'], Host))
974 F.write("; Errors\n")
977 # Oops, something unspeakable happened.
983 def HostToIP(Host, mapped=True):
987 if Host[1].has_key("ipHostNumber"):
988 for addr in Host[1]["ipHostNumber"]:
989 IPAdresses.append(addr)
990 if not is_ipv6_addr(addr) and mapped == "True":
991 IPAdresses.append("::ffff:"+addr)
995 # Generate the ssh known hosts file
996 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
999 OldMask = os.umask(0022)
1000 F = open(File + ".tmp", "w", 0644)
1003 for x in host_attrs:
1004 if x[1].has_key("hostname") == 0 or \
1005 x[1].has_key("sshRSAHostKey") == 0:
1007 Host = GetAttr(x, "hostname")
1008 HostNames = [ Host ]
1009 if Host.endswith(HostDomain):
1010 HostNames.append(Host[:-(len(HostDomain) + 1)])
1012 # in the purpose field [[host|some other text]] (where some other text is optional)
1013 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1014 # file. But so that we don't have to add everything we link we can add an asterisk
1015 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
1016 # http linking it we also support [[-hostname]] entries.
1017 for i in x[1].get("purpose", []):
1018 m = PurposeHostField.match(i)
1021 # we ignore [[*..]] entries
1022 if m.startswith('*'):
1024 if m.startswith('-'):
1028 if m.endswith(HostDomain):
1029 HostNames.append(m[:-(len(HostDomain) + 1)])
1031 for I in x[1]["sshRSAHostKey"]:
1032 if mode and mode == 'authorized_keys':
1034 if 'sshdistAuthKeysHost' in x[1]:
1035 hosts += x[1]['sshdistAuthKeysHost']
1036 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1037 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1038 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1040 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1041 Line = Sanitize(Line) + "\n"
1043 # Oops, something unspeakable happened.
1049 # Generate the debianhosts file (list of all IP addresses)
1050 def GenHosts(host_attrs, File):
1053 OldMask = os.umask(0022)
1054 F = open(File + ".tmp", "w", 0644)
1059 for x in host_attrs:
1061 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1064 if not 'ipHostNumber' in x[1]:
1067 addrs = x[1]["ipHostNumber"]
1069 if addr not in seen:
1071 addr = Sanitize(addr) + "\n"
1074 # Oops, something unspeakable happened.
1080 def replaceTree(src, dst_basedir):
1081 bn = os.path.basename(src)
1082 dst = os.path.join(dst_basedir, bn)
1084 shutil.copytree(src, dst)
1086 def GenKeyrings(OutDir):
1088 if os.path.isdir(k):
1089 replaceTree(k, OutDir)
1091 shutil.copy(k, OutDir)
1094 def get_accounts(ldap_conn):
1095 # Fetch all the users
1096 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1097 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1098 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1099 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1100 "shadowExpire", "emailForward", "latitude", "longitude",\
1101 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1102 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1103 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1104 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1105 "mailContentInspectionAction", "webPassword", "voipPassword"])
1107 if passwd_attrs is None:
1108 raise UDEmptyList, "No Users"
1109 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1110 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1114 def get_hosts(ldap_conn):
1115 # Fetch all the hosts
1116 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1117 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1118 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1120 if HostAttrs == None:
1121 raise UDEmptyList, "No Hosts"
1123 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1128 def make_ldap_conn():
1129 # Connect to the ldap server
1131 # for testing purposes it's sometimes useful to pass username/password
1132 # via the environment
1133 if 'UD_CREDENTIALS' in os.environ:
1134 Pass = os.environ['UD_CREDENTIALS'].split()
1136 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1137 Pass = F.readline().strip().split(" ")
1139 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1145 def setup_group_maps(l):
1146 # Fetch all the groups
1149 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1150 ["gid", "gidNumber", "subGroup"])
1152 # Generate the subgroup_map and group_id_map
1154 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1156 if x[1].has_key("gidNumber") == 0:
1158 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1159 if x[1].has_key("subGroup") != 0:
1160 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1164 SubGroupMap = subgroup_map
1165 GroupIDMap = group_id_map
1167 def generate_all(global_dir, ldap_conn):
1168 accounts = get_accounts(ldap_conn)
1169 host_attrs = get_hosts(ldap_conn)
1172 # Generate global things
1173 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1175 accounts = filter(lambda x: not IsRetired(x), accounts)
1176 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1178 CheckForward(accounts)
1180 GenMailDisable(accounts, global_dir + "mail-disable")
1181 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1182 GenDBM(accounts, global_dir + "mail-forward.db", 'emailForward')
1183 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1184 GenDBM(accounts, global_dir + "mail-contentinspectionaction.db", 'mailContentInspectionAction')
1185 GenPrivate(accounts, global_dir + "debian-private")
1186 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1187 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1188 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1189 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1190 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1191 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1192 GenWebPassword(accounts, global_dir + "web-passwords")
1193 GenVoipPassword(accounts, global_dir + "voip-passwords")
1194 GenKeyrings(global_dir)
1197 GenForward(accounts, global_dir + "forward-alias")
1199 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1200 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1202 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1203 GenMarkers(accounts, global_dir + "markers")
1204 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1205 GenHosts(host_attrs, global_dir + "debianhosts")
1206 GenSSHGitolite(accounts, host_attrs, global_dir + "ssh-gitolite")
1208 GenDNS(accounts, global_dir + "dns-zone")
1209 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1211 setup_group_maps(ldap_conn)
1213 for host in host_attrs:
1214 if not "hostname" in host[1]:
1216 generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1218 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1219 current_host = host[1]['hostname'][0]
1220 OutDir = global_dir + current_host + '/'
1221 if not os.path.isdir(OutDir):
1224 # Get the group list and convert any named groups to numerics
1226 for groupname in AllowedGroupsPreload.strip().split(" "):
1227 GroupList[groupname] = True
1228 if 'allowedGroups' in host[1]:
1229 for groupname in host[1]['allowedGroups']:
1230 GroupList[groupname] = True
1231 for groupname in GroupList.keys():
1232 if groupname in GroupIDMap:
1233 GroupList[str(GroupIDMap[groupname])] = True
1236 if 'exportOptions' in host[1]:
1237 for extra in host[1]['exportOptions']:
1238 ExtraList[extra.upper()] = True
1241 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1243 DoLink(global_dir, OutDir, "debianhosts")
1244 DoLink(global_dir, OutDir, "ssh_known_hosts")
1245 DoLink(global_dir, OutDir, "disabled-accounts")
1248 if 'NOPASSWD' in ExtraList:
1249 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1251 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1253 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1254 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1256 # Now we know who we're allowing on the machine, export
1257 # the relevant ssh keys
1258 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1260 if not 'NOPASSWD' in ExtraList:
1261 GenShadow(accounts, OutDir + "shadow")
1263 # Link in global things
1264 if not 'NOMARKERS' in ExtraList:
1265 DoLink(global_dir, OutDir, "markers")
1266 DoLink(global_dir, OutDir, "mail-forward.cdb")
1267 DoLink(global_dir, OutDir, "mail-forward.db")
1268 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1269 DoLink(global_dir, OutDir, "mail-contentinspectionaction.db")
1270 DoLink(global_dir, OutDir, "mail-disable")
1271 DoLink(global_dir, OutDir, "mail-greylist")
1272 DoLink(global_dir, OutDir, "mail-callout")
1273 DoLink(global_dir, OutDir, "mail-rbl")
1274 DoLink(global_dir, OutDir, "mail-rhsbl")
1275 DoLink(global_dir, OutDir, "mail-whitelist")
1276 DoLink(global_dir, OutDir, "all-accounts.json")
1277 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1278 GenDBM(accounts, OutDir + "user-forward.db", 'emailForward')
1279 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1280 GenDBM(accounts, OutDir + "batv-tokens.db", 'bATVToken')
1281 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1282 GenDBM(accounts, OutDir + "default-mail-options.db", 'mailDefaultOptions')
1285 DoLink(global_dir, OutDir, "forward-alias")
1287 if 'DNS' in ExtraList:
1288 DoLink(global_dir, OutDir, "dns-zone")
1289 DoLink(global_dir, OutDir, "dns-sshfp")
1291 if 'AUTHKEYS' in ExtraList:
1292 DoLink(global_dir, OutDir, "authorized_keys")
1294 if 'BSMTP' in ExtraList:
1295 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1297 if 'PRIVATE' in ExtraList:
1298 DoLink(global_dir, OutDir, "debian-private")
1300 if 'GITOLITE' in ExtraList:
1301 DoLink(global_dir, OutDir, "ssh-gitolite")
1302 if 'exportOptions' in host[1]:
1303 for entry in host[1]['exportOptions']:
1304 v = entry.split('=',1)
1305 if v[0] != 'GITOLITE' or len(v) != 2: continue
1306 gitolite_accounts = filter(lambda x: IsInGroup(x, [v[1]], current_host), all_accounts)
1307 gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1308 GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(v[1],))
1310 if 'WEB-PASSWORDS' in ExtraList:
1311 DoLink(global_dir, OutDir, "web-passwords")
1313 if 'VOIP-PASSWORDS' in ExtraList:
1314 DoLink(global_dir, OutDir, "voip-passwords")
1316 if 'KEYRING' in ExtraList:
1318 bn = os.path.basename(k)
1319 if os.path.isdir(k):
1320 src = os.path.join(global_dir, bn)
1321 replaceTree(src, OutDir)
1323 DoLink(global_dir, OutDir, bn)
1327 bn = os.path.basename(k)
1328 target = os.path.join(OutDir, bn)
1329 if os.path.isdir(target):
1332 posix.remove(target)
1335 DoLink(global_dir, OutDir, "last_update.trace")
1338 def getLastLDAPChangeTime(l):
1339 mods = l.search_s('cn=log',
1340 ldap.SCOPE_ONELEVEL,
1341 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1346 # Sort the list by reqEnd
1347 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1348 # Take the last element in the array
1349 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1353 def getLastKeyringChangeTime():
1356 mt = os.path.getmtime(k)
1362 def getLastBuildTime(gdir):
1363 cache_last_ldap_mod = 0
1364 cache_last_unix_mod = 0
1368 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1369 cache_last_mod=fd.read().split()
1371 cache_last_ldap_mod = cache_last_mod[0]
1372 cache_last_unix_mod = int(cache_last_mod[1])
1373 cache_last_run = int(cache_last_mod[2])
1374 except IndexError, ValueError:
1378 if e.errno == errno.ENOENT:
1383 return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1386 parser = optparse.OptionParser()
1387 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1388 help="Output directory.")
1389 parser.add_option("-f", "--force", dest="force", action="store_true",
1390 help="Force generation, even if no update to LDAP has happened.")
1392 (options, args) = parser.parse_args()
1397 if options.generatedir is not None:
1398 generate_dir = os.environ['UD_GENERATEDIR']
1399 elif 'UD_GENERATEDIR' in os.environ:
1400 generate_dir = os.environ['UD_GENERATEDIR']
1402 generate_dir = GenerateDir
1405 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1406 lock = get_lock( lockf )
1408 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1411 l = make_ldap_conn()
1413 time_started = int(time.time())
1414 ldap_last_mod = getLastLDAPChangeTime(l)
1415 unix_last_mod = getLastKeyringChangeTime()
1416 cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1418 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)
1420 if not options.force and not need_update:
1421 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1422 fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1426 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1427 generate_all(generate_dir, l)
1428 tracefd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, time_started))
1432 if __name__ == "__main__":
1433 if 'UD_PROFILE' in os.environ:
1436 cProfile.run('ud_generate()', "udg_prof")
1437 p = pstats.Stats('udg_prof')
1438 ##p.sort_stats('time').print_stats()
1439 p.sort_stats('cumulative').print_stats()
1445 # vim:set shiftwidth=3: