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 Pass = str(a['voipPassword'])
426 realm = 'sip.debian.org'
428 A1 = "%s:%s:%s" % (a['uid'], realm, Pass)
429 HA1 = hashlib.md5(A1).hexdigest()
430 Line = "%s:%s:%s:AUTHORIZED" % (a['uid'], HA1, realm)
431 Line = Sanitize(Line) + "\n"
432 F.write("%s" % (Line))
438 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
439 OldMask = os.umask(0077)
440 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
443 if f not in ssh_userkeys:
445 # If we're not exporting their primary group, don't export
448 if userlist[f] in grouprevmap.keys():
449 grname = grouprevmap[userlist[f]]
452 if int(userlist[f]) <= 100:
453 # In these cases, look it up in the normal way so we
454 # deal with cases where, for instance, users are in group
455 # users as their primary group.
456 grname = grp.getgrgid(userlist[f])[0]
461 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])
465 for line in ssh_userkeys[f]:
466 if line.startswith("allowed_hosts=") and ' ' in line:
467 machines, line = line.split('=', 1)[1].split(' ', 1)
468 if current_host not in machines.split(','):
469 continue # skip this key
472 continue # no keys for this host
473 contents = "\n".join(lines) + "\n"
475 to = tarfile.TarInfo(name=f)
476 # These will only be used where the username doesn't
477 # exist on the target system for some reason; hence,
478 # in those cases, the safest thing is for the file to
479 # be owned by root but group nobody. This deals with
480 # the bloody obscure case where the group fails to exist
481 # whilst the user does (in which case we want to avoid
482 # ending up with a file which is owned user:root to avoid
483 # a fairly obvious attack vector)
486 # Using the username / groupname fields avoids any need
487 # to give a shit^W^W^Wcare about the UIDoffset stuff.
491 to.mtime = int(time.time())
492 to.size = len(contents)
494 tf.addfile(to, StringIO(contents))
497 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
499 # add a list of groups to existing groups,
500 # including all subgroups thereof, recursively.
501 # basically this proceduces the transitive hull of the groups in
503 def addGroups(existingGroups, newGroups, uid, current_host):
504 for group in newGroups:
505 # if it's a <group>@host, split it and verify it's on the current host.
506 s = group.split('@', 1)
507 if len(s) == 2 and s[1] != current_host:
511 # let's see if we handled this group already
512 if group in existingGroups:
515 if not GroupIDMap.has_key(group):
516 print "Group", group, "does not exist but", uid, "is in it"
519 existingGroups.append(group)
521 if SubGroupMap.has_key(group):
522 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
524 # Generate the group list
525 def GenGroup(accounts, File, current_host):
529 F = open(File + ".tdb.tmp", "w")
531 # Generate the GroupMap
535 GroupHasPrimaryMembers = {}
537 # Sort them into a list of groups having a set of users
539 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
540 if not 'supplementaryGid' in a: continue
543 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
545 GroupMap[g].append(a['uid'])
547 # Output the group file.
549 for x in GroupMap.keys():
550 if not x in GroupIDMap:
553 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
556 grouprevmap[GroupIDMap[x]] = x
558 Line = "%s:x:%u:" % (x, GroupIDMap[x])
560 for I in GroupMap[x]:
561 Line = Line + ("%s%s" % (Comma, I))
563 Line = Sanitize(Line) + "\n"
564 F.write("0%u %s" % (J, Line))
565 F.write(".%s %s" % (x, Line))
566 F.write("=%u %s" % (GroupIDMap[x], Line))
569 # Oops, something unspeakable happened.
577 def CheckForward(accounts):
579 if not 'emailForward' in a: continue
583 # Do not allow people to try to buffer overflow busted parsers
584 if len(a['emailForward']) > 200: delete = True
585 # Check the forwarding address
586 elif EmailCheck.match(a['emailForward']) is None: delete = True
589 a.delete_mailforward()
591 # Generate the email forwarding list
592 def GenForward(accounts, File):
595 OldMask = os.umask(0022)
596 F = open(File + ".tmp", "w", 0644)
600 if not 'emailForward' in a: continue
601 Line = "%s: %s" % (a['uid'], a['emailForward'])
602 Line = Sanitize(Line) + "\n"
605 # Oops, something unspeakable happened.
611 def GenCDB(accounts, File, key):
614 OldMask = os.umask(0022)
615 # nothing else does the fsync stuff, so why do it here?
616 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
617 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
620 # Write out the email address for each user
622 if not key in a: continue
625 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
628 # Oops, something unspeakable happened.
632 if Fdb.close() != None:
633 raise "cdbmake gave an error"
635 def GenDBM(accounts, File, key):
637 OldMask = os.umask(0022)
638 fn = os.path.join(File).encode('ascii', 'ignore')
645 Fdb = dbm.open(fn, "c")
648 # Write out the email address for each user
650 if not key in a: continue
657 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
658 os.remove(File + ".db")
660 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
661 os.rename (File + ".db", File)
663 # Generate the anon XEarth marker file
664 def GenMarkers(accounts, File):
667 F = open(File + ".tmp", "w")
669 # Write out the position for each user
671 if not ('latitude' in a and 'longitude' in a): continue
673 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
674 Line = Sanitize(Line) + "\n"
679 # Oops, something unspeakable happened.
685 # Generate the debian-private subscription list
686 def GenPrivate(accounts, File):
689 F = open(File + ".tmp", "w")
691 # Write out the position for each user
693 if not a.is_active_user(): continue
694 if a.is_guest_account(): continue
695 if not 'privateSub' in a: continue
697 Line = "%s"%(a['privateSub'])
698 Line = Sanitize(Line) + "\n"
703 # Oops, something unspeakable happened.
709 # Generate a list of locked accounts
710 def GenDisabledAccounts(accounts, File):
713 F = open(File + ".tmp", "w")
714 disabled_accounts = []
716 # Fetch all the users
718 if a.pw_active(): continue
719 Line = "%s:%s" % (a['uid'], "Account is locked")
720 disabled_accounts.append(a)
721 F.write(Sanitize(Line) + "\n")
723 # Oops, something unspeakable happened.
728 return disabled_accounts
730 # Generate the list of local addresses that refuse all mail
731 def GenMailDisable(accounts, File):
734 F = open(File + ".tmp", "w")
737 if not 'mailDisableMessage' in a: continue
738 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
739 Line = Sanitize(Line) + "\n"
742 # Oops, something unspeakable happened.
748 # Generate a list of uids that should have boolean affects applied
749 def GenMailBool(accounts, File, key):
752 F = open(File + ".tmp", "w")
755 if not key in a: continue
756 if not a[key] == 'TRUE': continue
757 Line = "%s"%(a['uid'])
758 Line = Sanitize(Line) + "\n"
761 # Oops, something unspeakable happened.
767 # Generate a list of hosts for RBL or whitelist purposes.
768 def GenMailList(accounts, File, key):
771 F = open(File + ".tmp", "w")
773 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
774 else: validregex = re.compile('^[-\w.]+$')
777 if not key in a: continue
779 filtered = filter(lambda z: validregex.match(z), a[key])
780 if len(filtered) == 0: continue
781 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
782 line = a['uid'] + ': ' + ' : '.join(filtered)
783 line = Sanitize(line) + "\n"
786 # Oops, something unspeakable happened.
792 def isRoleAccount(account):
793 return 'debianRoleAccount' in account['objectClass']
795 # Generate the DNS Zone file
796 def GenDNS(accounts, File):
799 F = open(File + ".tmp", "w")
801 # Fetch all the users
804 # Write out the zone file entry for each user
806 if not 'dnsZoneEntry' in a: continue
807 if not a.is_active_user() and not isRoleAccount(a): continue
808 if a.is_guest_account(): continue
811 F.write("; %s\n"%(a.email_address()))
812 for z in a["dnsZoneEntry"]:
813 Split = z.lower().split()
814 if Split[1].lower() == 'in':
815 Line = " ".join(Split) + "\n"
818 Host = Split[0] + DNSZone
819 if BSMTPCheck.match(Line) != None:
820 F.write("; Has BSMTP\n")
822 # Write some identification information
823 if not RRs.has_key(Host):
824 if Split[2].lower() in ["a", "aaaa"]:
825 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
826 for y in a["keyFingerPrint"]:
827 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
831 Line = "; Err %s"%(str(Split))
836 F.write("; Errors:\n")
837 for line in str(e).split("\n"):
838 F.write("; %s\n"%(line))
841 # Oops, something unspeakable happened.
849 socket.inet_pton(socket.AF_INET6, i)
854 def ExtractDNSInfo(x):
858 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
861 if x[1].has_key("ipHostNumber"):
862 for I in x[1]["ipHostNumber"]:
864 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
866 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
870 if 'sshRSAHostKey' in x[1]:
871 for I in x[1]["sshRSAHostKey"]:
873 if Split[0] == 'ssh-rsa':
875 if Split[0] == 'ssh-dss':
877 if Algorithm == None:
879 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
880 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
882 if 'architecture' in x[1]:
883 Arch = GetAttr(x, "architecture")
885 if x[1].has_key("machine"):
886 Mach = " " + GetAttr(x, "machine")
887 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
889 if x[1].has_key("mXRecord"):
890 for I in x[1]["mXRecord"]:
892 for e in MX_remap[I]:
893 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
895 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
899 # Generate the DNS records
900 def GenZoneRecords(host_attrs, File):
903 F = open(File + ".tmp", "w")
905 # Fetch all the hosts
907 if x[1].has_key("hostname") == 0:
910 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
913 DNSInfo = ExtractDNSInfo(x)
917 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
920 Line = "\t\t\t%s" % (Line)
924 # this would write sshfp lines for services on machines
925 # but we can't yet, since some are cnames and we'll make
926 # an invalid zonefile
928 # for i in x[1].get("purpose", []):
929 # m = PurposeHostField.match(i)
932 # # we ignore [[*..]] entries
933 # if m.startswith('*'):
935 # if m.startswith('-'):
938 # if not m.endswith(HostDomain):
940 # if not m.endswith('.'):
942 # for Line in DNSInfo:
943 # if isSSHFP.match(Line):
944 # Line = "%s\t%s" % (m, Line)
945 # F.write(Line + "\n")
947 # Oops, something unspeakable happened.
953 # Generate the BSMTP file
954 def GenBSMTP(accounts, File, HomePrefix):
957 F = open(File + ".tmp", "w")
959 # Write out the zone file entry for each user
961 if not 'dnsZoneEntry' in a: continue
962 if not a.is_active_user(): continue
965 for z in a["dnsZoneEntry"]:
966 Split = z.lower().split()
967 if Split[1].lower() == 'in':
968 for y in range(0, len(Split)):
971 Line = " ".join(Split) + "\n"
973 Host = Split[0] + DNSZone
974 if BSMTPCheck.match(Line) != None:
975 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
976 a['uid'], HomePrefix, a['uid'], Host))
979 F.write("; Errors\n")
982 # Oops, something unspeakable happened.
988 def HostToIP(Host, mapped=True):
992 if Host[1].has_key("ipHostNumber"):
993 for addr in Host[1]["ipHostNumber"]:
994 IPAdresses.append(addr)
995 if not is_ipv6_addr(addr) and mapped == "True":
996 IPAdresses.append("::ffff:"+addr)
1000 # Generate the ssh known hosts file
1001 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
1004 OldMask = os.umask(0022)
1005 F = open(File + ".tmp", "w", 0644)
1008 for x in host_attrs:
1009 if x[1].has_key("hostname") == 0 or \
1010 x[1].has_key("sshRSAHostKey") == 0:
1012 Host = GetAttr(x, "hostname")
1013 HostNames = [ Host ]
1014 if Host.endswith(HostDomain):
1015 HostNames.append(Host[:-(len(HostDomain) + 1)])
1017 # in the purpose field [[host|some other text]] (where some other text is optional)
1018 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1019 # file. But so that we don't have to add everything we link we can add an asterisk
1020 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
1021 # http linking it we also support [[-hostname]] entries.
1022 for i in x[1].get("purpose", []):
1023 m = PurposeHostField.match(i)
1026 # we ignore [[*..]] entries
1027 if m.startswith('*'):
1029 if m.startswith('-'):
1033 if m.endswith(HostDomain):
1034 HostNames.append(m[:-(len(HostDomain) + 1)])
1036 for I in x[1]["sshRSAHostKey"]:
1037 if mode and mode == 'authorized_keys':
1039 if 'sshdistAuthKeysHost' in x[1]:
1040 hosts += x[1]['sshdistAuthKeysHost']
1041 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1042 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1043 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1045 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1046 Line = Sanitize(Line) + "\n"
1048 # Oops, something unspeakable happened.
1054 # Generate the debianhosts file (list of all IP addresses)
1055 def GenHosts(host_attrs, File):
1058 OldMask = os.umask(0022)
1059 F = open(File + ".tmp", "w", 0644)
1064 for x in host_attrs:
1066 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1069 if not 'ipHostNumber' in x[1]:
1072 addrs = x[1]["ipHostNumber"]
1074 if addr not in seen:
1076 addr = Sanitize(addr) + "\n"
1079 # Oops, something unspeakable happened.
1085 def replaceTree(src, dst_basedir):
1086 bn = os.path.basename(src)
1087 dst = os.path.join(dst_basedir, bn)
1089 shutil.copytree(src, dst)
1091 def GenKeyrings(OutDir):
1093 if os.path.isdir(k):
1094 replaceTree(k, OutDir)
1096 shutil.copy(k, OutDir)
1099 def get_accounts(ldap_conn):
1100 # Fetch all the users
1101 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1102 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1103 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1104 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1105 "shadowExpire", "emailForward", "latitude", "longitude",\
1106 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1107 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1108 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1109 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1110 "mailContentInspectionAction", "webPassword", "voipPassword"])
1112 if passwd_attrs is None:
1113 raise UDEmptyList, "No Users"
1114 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1115 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1119 def get_hosts(ldap_conn):
1120 # Fetch all the hosts
1121 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1122 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1123 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1125 if HostAttrs == None:
1126 raise UDEmptyList, "No Hosts"
1128 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1133 def make_ldap_conn():
1134 # Connect to the ldap server
1136 # for testing purposes it's sometimes useful to pass username/password
1137 # via the environment
1138 if 'UD_CREDENTIALS' in os.environ:
1139 Pass = os.environ['UD_CREDENTIALS'].split()
1141 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1142 Pass = F.readline().strip().split(" ")
1144 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1150 def setup_group_maps(l):
1151 # Fetch all the groups
1154 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1155 ["gid", "gidNumber", "subGroup"])
1157 # Generate the subgroup_map and group_id_map
1159 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1161 if x[1].has_key("gidNumber") == 0:
1163 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1164 if x[1].has_key("subGroup") != 0:
1165 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1169 SubGroupMap = subgroup_map
1170 GroupIDMap = group_id_map
1172 def generate_all(global_dir, ldap_conn):
1173 accounts = get_accounts(ldap_conn)
1174 host_attrs = get_hosts(ldap_conn)
1177 # Generate global things
1178 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1180 accounts = filter(lambda x: not IsRetired(x), accounts)
1181 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1183 CheckForward(accounts)
1185 GenMailDisable(accounts, global_dir + "mail-disable")
1186 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1187 GenDBM(accounts, global_dir + "mail-forward.db", 'emailForward')
1188 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1189 GenDBM(accounts, global_dir + "mail-contentinspectionaction.db", 'mailContentInspectionAction')
1190 GenPrivate(accounts, global_dir + "debian-private")
1191 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1192 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1193 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1194 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1195 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1196 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1197 GenWebPassword(accounts, global_dir + "web-passwords")
1198 GenVoipPassword(accounts, global_dir + "voip-passwords")
1199 GenKeyrings(global_dir)
1202 GenForward(accounts, global_dir + "forward-alias")
1204 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1205 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1207 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1208 GenMarkers(accounts, global_dir + "markers")
1209 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1210 GenHosts(host_attrs, global_dir + "debianhosts")
1211 GenSSHGitolite(accounts, host_attrs, global_dir + "ssh-gitolite")
1213 GenDNS(accounts, global_dir + "dns-zone")
1214 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1216 setup_group_maps(ldap_conn)
1218 for host in host_attrs:
1219 if not "hostname" in host[1]:
1221 generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1223 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1224 current_host = host[1]['hostname'][0]
1225 OutDir = global_dir + current_host + '/'
1226 if not os.path.isdir(OutDir):
1229 # Get the group list and convert any named groups to numerics
1231 for groupname in AllowedGroupsPreload.strip().split(" "):
1232 GroupList[groupname] = True
1233 if 'allowedGroups' in host[1]:
1234 for groupname in host[1]['allowedGroups']:
1235 GroupList[groupname] = True
1236 for groupname in GroupList.keys():
1237 if groupname in GroupIDMap:
1238 GroupList[str(GroupIDMap[groupname])] = True
1241 if 'exportOptions' in host[1]:
1242 for extra in host[1]['exportOptions']:
1243 ExtraList[extra.upper()] = True
1246 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1248 DoLink(global_dir, OutDir, "debianhosts")
1249 DoLink(global_dir, OutDir, "ssh_known_hosts")
1250 DoLink(global_dir, OutDir, "disabled-accounts")
1253 if 'NOPASSWD' in ExtraList:
1254 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1256 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1258 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1259 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1261 # Now we know who we're allowing on the machine, export
1262 # the relevant ssh keys
1263 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1265 if not 'NOPASSWD' in ExtraList:
1266 GenShadow(accounts, OutDir + "shadow")
1268 # Link in global things
1269 if not 'NOMARKERS' in ExtraList:
1270 DoLink(global_dir, OutDir, "markers")
1271 DoLink(global_dir, OutDir, "mail-forward.cdb")
1272 DoLink(global_dir, OutDir, "mail-forward.db")
1273 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1274 DoLink(global_dir, OutDir, "mail-contentinspectionaction.db")
1275 DoLink(global_dir, OutDir, "mail-disable")
1276 DoLink(global_dir, OutDir, "mail-greylist")
1277 DoLink(global_dir, OutDir, "mail-callout")
1278 DoLink(global_dir, OutDir, "mail-rbl")
1279 DoLink(global_dir, OutDir, "mail-rhsbl")
1280 DoLink(global_dir, OutDir, "mail-whitelist")
1281 DoLink(global_dir, OutDir, "all-accounts.json")
1282 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1283 GenDBM(accounts, OutDir + "user-forward.db", 'emailForward')
1284 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1285 GenDBM(accounts, OutDir + "batv-tokens.db", 'bATVToken')
1286 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1287 GenDBM(accounts, OutDir + "default-mail-options.db", 'mailDefaultOptions')
1290 DoLink(global_dir, OutDir, "forward-alias")
1292 if 'DNS' in ExtraList:
1293 DoLink(global_dir, OutDir, "dns-zone")
1294 DoLink(global_dir, OutDir, "dns-sshfp")
1296 if 'AUTHKEYS' in ExtraList:
1297 DoLink(global_dir, OutDir, "authorized_keys")
1299 if 'BSMTP' in ExtraList:
1300 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1302 if 'PRIVATE' in ExtraList:
1303 DoLink(global_dir, OutDir, "debian-private")
1305 if 'GITOLITE' in ExtraList:
1306 DoLink(global_dir, OutDir, "ssh-gitolite")
1307 if 'exportOptions' in host[1]:
1308 for entry in host[1]['exportOptions']:
1309 v = entry.split('=',1)
1310 if v[0] != 'GITOLITE' or len(v) != 2: continue
1311 gitolite_accounts = filter(lambda x: IsInGroup(x, [v[1]], current_host), all_accounts)
1312 gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1313 GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(v[1],))
1315 if 'WEB-PASSWORDS' in ExtraList:
1316 DoLink(global_dir, OutDir, "web-passwords")
1318 if 'VOIP-PASSWORDS' in ExtraList:
1319 DoLink(global_dir, OutDir, "voip-passwords")
1321 if 'KEYRING' in ExtraList:
1323 bn = os.path.basename(k)
1324 if os.path.isdir(k):
1325 src = os.path.join(global_dir, bn)
1326 replaceTree(src, OutDir)
1328 DoLink(global_dir, OutDir, bn)
1332 bn = os.path.basename(k)
1333 target = os.path.join(OutDir, bn)
1334 if os.path.isdir(target):
1337 posix.remove(target)
1340 DoLink(global_dir, OutDir, "last_update.trace")
1343 def getLastLDAPChangeTime(l):
1344 mods = l.search_s('cn=log',
1345 ldap.SCOPE_ONELEVEL,
1346 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1351 # Sort the list by reqEnd
1352 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1353 # Take the last element in the array
1354 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1358 def getLastKeyringChangeTime():
1361 mt = os.path.getmtime(k)
1367 def getLastBuildTime(gdir):
1368 cache_last_ldap_mod = 0
1369 cache_last_unix_mod = 0
1373 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1374 cache_last_mod=fd.read().split()
1376 cache_last_ldap_mod = cache_last_mod[0]
1377 cache_last_unix_mod = int(cache_last_mod[1])
1378 cache_last_run = int(cache_last_mod[2])
1379 except IndexError, ValueError:
1383 if e.errno == errno.ENOENT:
1388 return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1391 parser = optparse.OptionParser()
1392 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1393 help="Output directory.")
1394 parser.add_option("-f", "--force", dest="force", action="store_true",
1395 help="Force generation, even if no update to LDAP has happened.")
1397 (options, args) = parser.parse_args()
1402 if options.generatedir is not None:
1403 generate_dir = os.environ['UD_GENERATEDIR']
1404 elif 'UD_GENERATEDIR' in os.environ:
1405 generate_dir = os.environ['UD_GENERATEDIR']
1407 generate_dir = GenerateDir
1410 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1411 lock = get_lock( lockf )
1413 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1416 l = make_ldap_conn()
1418 time_started = int(time.time())
1419 ldap_last_mod = getLastLDAPChangeTime(l)
1420 unix_last_mod = getLastKeyringChangeTime()
1421 cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1423 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)
1425 if not options.force and not need_update:
1426 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1427 fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1431 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1432 generate_all(generate_dir, l)
1433 tracefd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, time_started))
1437 if __name__ == "__main__":
1438 if 'UD_PROFILE' in os.environ:
1441 cProfile.run('ud_generate()', "udg_prof")
1442 p = pstats.Stats('udg_prof')
1443 ##p.sort_stats('time').print_stats()
1444 p.sort_stats('cumulative').print_stats()
1450 # vim:set shiftwidth=3: