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 from dsa_mq.connection import Connection
32 from dsa_mq.config import Config
34 import string, re, time, ldap, optparse, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp, fcntl, dbm
35 from userdir_ldap import *
36 from userdir_exceptions import *
38 from xml.etree.ElementTree import Element, SubElement, Comment
39 from xml.etree import ElementTree
40 from xml.dom import minidom
42 from cStringIO import StringIO
44 from StringIO import StringIO
46 import simplejson as json
49 if not '__author__' in json.__dict__:
50 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
51 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
54 sys.stderr.write("You should probably not run ud-generate as root.\n")
66 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
69 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)(,\s*([^ <>@]+@[^ ,<>@]+))*$")
70 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
71 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
72 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
73 isSSHFP = re.compile("^\s*IN\s+SSHFP")
74 DNSZone = ".debian.net"
75 Keyrings = ConfModule.sync_keyrings.split(":")
76 GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
77 GitoliteSSHCommand = getattr(ConfModule, "gitolitesshcommand", None)
78 GitoliteExportHosts = re.compile(getattr(ConfModule, "gitoliteexporthosts", "."))
79 MX_remap = json.loads(ConfModule.MX_remap)
80 use_mq = getattr(ConfModule, "use_mq", True)
82 rtc_realm = getattr(ConfModule, "rtc_realm", None)
83 rtc_append = getattr(ConfModule, "rtc_append", None)
86 """Return a pretty-printed XML string for the Element.
88 rough_string = ElementTree.tostring(elem, 'utf-8')
89 reparsed = minidom.parseString(rough_string)
90 return reparsed.toprettyxml(indent=" ")
92 def safe_makedirs(dir):
96 if e.errno == errno.EEXIST:
101 def safe_rmtree(dir):
105 if e.errno == errno.ENOENT:
110 def get_lock(fn, wait=5*60):
113 ends = time.time() + wait
118 fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
122 if time.time() >= ends:
124 sl = min(sl*2, 10, ends - time.time())
130 return Str.translate(string.maketrans("\n\r\t", "$$$"))
132 def DoLink(From, To, File):
134 posix.remove(To + File)
137 posix.link(From + File, To + File)
139 def IsRetired(account):
141 Looks for accountStatus in the LDAP record and tries to
142 match it against one of the known retired statuses
145 status = account['accountStatus']
147 line = status.split()
150 if status == "inactive":
153 elif status == "memorial":
156 elif status == "retiring":
157 # We'll give them a few extra days over what we said
158 age = 6 * 31 * 24 * 60 * 60
160 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
168 # See if this user is in the group list
169 def IsInGroup(account, allowed, current_host):
170 # See if the primary group is in the list
171 if str(account['gidNumber']) in allowed: return True
173 # Check the host based ACL
174 if account.is_allowed_by_hostacl(current_host): return True
176 # See if there are supplementary groups
177 if not 'supplementaryGid' in account: return False
180 addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
186 def Die(File, F, Fdb):
192 os.remove(File + ".tmp")
196 os.remove(File + ".tdb.tmp")
200 def Done(File, F, Fdb):
203 os.rename(File + ".tmp", File)
206 os.rename(File + ".tdb.tmp", File + ".tdb")
208 # Generate the password list
209 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
212 F = open(File + ".tdb.tmp", "w")
217 # Do not let people try to buffer overflow some busted passwd parser.
218 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
220 userlist[a['uid']] = a['gidNumber']
221 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
227 HomePrefix, a['uid'],
229 line = Sanitize(line) + "\n"
230 F.write("0%u %s" % (i, line))
231 F.write(".%s %s" % (a['uid'], line))
232 F.write("=%d %s" % (a['uidNumber'], line))
235 # Oops, something unspeakable happened.
241 # Return the list of users so we know which keys to export
244 def GenAllUsers(accounts, file):
247 OldMask = os.umask(0022)
248 f = open(file + ".tmp", "w", 0644)
253 all.append( { 'uid': a['uid'],
254 'uidNumber': a['uidNumber'],
255 'active': a.pw_active() and a.shadow_active() } )
258 # Oops, something unspeakable happened.
264 # Generate the shadow list
265 def GenShadow(accounts, File):
268 OldMask = os.umask(0077)
269 F = open(File + ".tdb.tmp", "w", 0600)
274 # If the account is locked, mark it as such in shadow
275 # See Debian Bug #308229 for why we set it to 1 instead of 0
276 if not a.pw_active(): ShadowExpire = '1'
277 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
278 else: ShadowExpire = ''
281 values.append(a['uid'])
282 values.append(a.get_password())
283 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
284 if key in a: values.append(a[key])
285 else: values.append('')
286 values.append(ShadowExpire)
287 line = ':'.join(values)+':'
288 line = Sanitize(line) + "\n"
289 F.write("0%u %s" % (i, line))
290 F.write(".%s %s" % (a['uid'], line))
293 # Oops, something unspeakable happened.
299 # Generate the sudo passwd file
300 def GenShadowSudo(accounts, File, untrusted, current_host):
303 OldMask = os.umask(0077)
304 F = open(File + ".tmp", "w", 0600)
309 if 'sudoPassword' in a:
310 for entry in a['sudoPassword']:
311 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*-]+) ([^ ]+)$').match(entry)
314 uuid = Match.group(1)
315 status = Match.group(2)
316 hosts = Match.group(3)
317 cryptedpass = Match.group(4)
319 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
321 for_all = hosts == "*"
322 for_this_host = current_host in hosts.split(',')
323 if not (for_all or for_this_host):
325 # ignore * passwords for untrusted hosts, but copy host specific passwords
326 if for_all and untrusted:
329 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
334 Line = "%s:%s" % (a['uid'], Pass)
335 Line = Sanitize(Line) + "\n"
336 F.write("%s" % (Line))
338 # Oops, something unspeakable happened.
344 # Generate the sudo passwd file
345 def GenSSHGitolite(accounts, hosts, File, sshcommand=None, current_host=None):
347 if sshcommand is None:
348 sshcommand = GitoliteSSHCommand
350 OldMask = os.umask(0022)
351 F = open(File + ".tmp", "w", 0600)
354 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
356 if not 'sshRSAAuthKey' in a: continue
359 prefix = GitoliteSSHRestrictions
360 prefix = prefix.replace('@@COMMAND@@', sshcommand)
361 prefix = prefix.replace('@@USER@@', User)
362 for I in a["sshRSAAuthKey"]:
363 if I.startswith("allowed_hosts=") and ' ' in line:
364 if current_host is None:
366 machines, I = I.split('=', 1)[1].split(' ', 1)
367 if current_host not in machines.split(','):
368 continue # skip this key
370 if I.startswith('ssh-'):
371 line = "%s %s"%(prefix, I)
373 continue # do not allow keys with other restrictions that might conflict
374 line = Sanitize(line) + "\n"
377 for dn, attrs in hosts:
378 if not 'sshRSAHostKey' in attrs: continue
379 hostname = "host-" + attrs['hostname'][0]
380 prefix = GitoliteSSHRestrictions
381 prefix = prefix.replace('@@COMMAND@@', sshcommand)
382 prefix = prefix.replace('@@USER@@', hostname)
383 for I in attrs["sshRSAHostKey"]:
384 line = "%s %s"%(prefix, I)
385 line = Sanitize(line) + "\n"
388 # Oops, something unspeakable happened.
394 # Generate the shadow list
395 def GenSSHShadow(global_dir, accounts):
396 # Fetch all the users
400 if not 'sshRSAAuthKey' in a: continue
403 for I in a['sshRSAAuthKey']:
404 MultipleLine = "%s" % I
405 MultipleLine = Sanitize(MultipleLine)
406 contents.append(MultipleLine)
407 userkeys[a['uid']] = contents
410 # Generate the webPassword list
411 def GenWebPassword(accounts, File):
414 OldMask = os.umask(0077)
415 F = open(File, "w", 0600)
419 if not 'webPassword' in a: continue
420 if not a.pw_active(): continue
422 Pass = str(a['webPassword'])
423 Line = "%s:%s" % (a['uid'], Pass)
424 Line = Sanitize(Line) + "\n"
425 F.write("%s" % (Line))
431 # Generate the rtcPassword list
432 def GenRtcPassword(accounts, File):
435 OldMask = os.umask(0077)
436 F = open(File, "w", 0600)
440 if not 'rtcPassword' in a: continue
441 if not a.pw_active(): continue
443 Line = "%s%s:%s:%s:AUTHORIZED" % (a['uid'], rtc_append, str(a['rtcPassword']), rtc_realm)
444 Line = Sanitize(Line) + "\n"
445 F.write("%s" % (Line))
451 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
452 OldMask = os.umask(0077)
453 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
456 if f not in ssh_userkeys:
458 # If we're not exporting their primary group, don't export
461 if userlist[f] in grouprevmap.keys():
462 grname = grouprevmap[userlist[f]]
465 if int(userlist[f]) <= 100:
466 # In these cases, look it up in the normal way so we
467 # deal with cases where, for instance, users are in group
468 # users as their primary group.
469 grname = grp.getgrgid(userlist[f])[0]
474 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])
478 for line in ssh_userkeys[f]:
479 if line.startswith("allowed_hosts=") and ' ' in line:
480 machines, line = line.split('=', 1)[1].split(' ', 1)
481 if current_host not in machines.split(','):
482 continue # skip this key
485 continue # no keys for this host
486 contents = "\n".join(lines) + "\n"
488 to = tarfile.TarInfo(name=f)
489 # These will only be used where the username doesn't
490 # exist on the target system for some reason; hence,
491 # in those cases, the safest thing is for the file to
492 # be owned by root but group nobody. This deals with
493 # the bloody obscure case where the group fails to exist
494 # whilst the user does (in which case we want to avoid
495 # ending up with a file which is owned user:root to avoid
496 # a fairly obvious attack vector)
499 # Using the username / groupname fields avoids any need
500 # to give a shit^W^W^Wcare about the UIDoffset stuff.
504 to.mtime = int(time.time())
505 to.size = len(contents)
507 tf.addfile(to, StringIO(contents))
510 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
512 # add a list of groups to existing groups,
513 # including all subgroups thereof, recursively.
514 # basically this proceduces the transitive hull of the groups in
516 def addGroups(existingGroups, newGroups, uid, current_host):
517 for group in newGroups:
518 # if it's a <group>@host, split it and verify it's on the current host.
519 s = group.split('@', 1)
520 if len(s) == 2 and s[1] != current_host:
524 # let's see if we handled this group already
525 if group in existingGroups:
528 if not GroupIDMap.has_key(group):
529 print "Group", group, "does not exist but", uid, "is in it"
532 existingGroups.append(group)
534 if SubGroupMap.has_key(group):
535 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
537 # Generate the group list
538 def GenGroup(accounts, File, current_host):
542 F = open(File + ".tdb.tmp", "w")
544 # Generate the GroupMap
548 GroupHasPrimaryMembers = {}
550 # Sort them into a list of groups having a set of users
552 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
553 if not 'supplementaryGid' in a: continue
556 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
558 GroupMap[g].append(a['uid'])
560 # Output the group file.
562 for x in GroupMap.keys():
563 if not x in GroupIDMap:
566 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
569 grouprevmap[GroupIDMap[x]] = x
571 Line = "%s:x:%u:" % (x, GroupIDMap[x])
573 for I in GroupMap[x]:
574 Line = Line + ("%s%s" % (Comma, I))
576 Line = Sanitize(Line) + "\n"
577 F.write("0%u %s" % (J, Line))
578 F.write(".%s %s" % (x, Line))
579 F.write("=%u %s" % (GroupIDMap[x], Line))
582 # Oops, something unspeakable happened.
590 def CheckForward(accounts):
592 if not 'emailForward' in a: continue
596 # Do not allow people to try to buffer overflow busted parsers
597 if len(a['emailForward']) > 200: delete = True
598 # Check the forwarding address
599 elif EmailCheck.match(a['emailForward']) is None: delete = True
602 a.delete_mailforward()
604 # Generate the email forwarding list
605 def GenForward(accounts, File):
608 OldMask = os.umask(0022)
609 F = open(File + ".tmp", "w", 0644)
613 if not 'emailForward' in a: continue
614 Line = "%s: %s" % (a['uid'], a['emailForward'])
615 Line = Sanitize(Line) + "\n"
618 # Oops, something unspeakable happened.
624 def GenCDB(accounts, File, key):
627 OldMask = os.umask(0022)
628 # nothing else does the fsync stuff, so why do it here?
629 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
630 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
633 # Write out the email address for each user
635 if not key in a: continue
638 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
641 # Oops, something unspeakable happened.
645 if Fdb.close() != None:
646 raise "cdbmake gave an error"
648 def GenDBM(accounts, File, key):
650 OldMask = os.umask(0022)
651 fn = os.path.join(File).encode('ascii', 'ignore')
658 Fdb = dbm.open(fn, "c")
661 # Write out the email address for each user
663 if not key in a: continue
670 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
671 os.remove(File + ".db")
673 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
674 os.rename (File + ".db", File)
676 # Generate the anon XEarth marker file
677 def GenMarkers(accounts, File):
680 F = open(File + ".tmp", "w")
682 # Write out the position for each user
684 if not ('latitude' in a and 'longitude' in a): continue
686 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
687 Line = Sanitize(Line) + "\n"
692 # Oops, something unspeakable happened.
698 # Generate the debian-private subscription list
699 def GenPrivate(accounts, File):
702 F = open(File + ".tmp", "w")
704 # Write out the position for each user
706 if not a.is_active_user(): continue
707 if a.is_guest_account(): continue
708 if not 'privateSub' in a: continue
710 Line = "%s"%(a['privateSub'])
711 Line = Sanitize(Line) + "\n"
716 # Oops, something unspeakable happened.
722 # Generate a list of locked accounts
723 def GenDisabledAccounts(accounts, File):
726 F = open(File + ".tmp", "w")
727 disabled_accounts = []
729 # Fetch all the users
731 if a.pw_active(): continue
732 Line = "%s:%s" % (a['uid'], "Account is locked")
733 disabled_accounts.append(a)
734 F.write(Sanitize(Line) + "\n")
736 # Oops, something unspeakable happened.
741 return disabled_accounts
743 # Generate the list of local addresses that refuse all mail
744 def GenMailDisable(accounts, File):
747 F = open(File + ".tmp", "w")
750 if not 'mailDisableMessage' in a: continue
751 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
752 Line = Sanitize(Line) + "\n"
755 # Oops, something unspeakable happened.
761 # Generate a list of uids that should have boolean affects applied
762 def GenMailBool(accounts, File, key):
765 F = open(File + ".tmp", "w")
768 if not key in a: continue
769 if not a[key] == 'TRUE': continue
770 Line = "%s"%(a['uid'])
771 Line = Sanitize(Line) + "\n"
774 # Oops, something unspeakable happened.
780 # Generate a list of hosts for RBL or whitelist purposes.
781 def GenMailList(accounts, File, key):
784 F = open(File + ".tmp", "w")
786 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
787 else: validregex = re.compile('^[-\w.]+$')
790 if not key in a: continue
792 filtered = filter(lambda z: validregex.match(z), a[key])
793 if len(filtered) == 0: continue
794 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
795 line = a['uid'] + ': ' + ' : '.join(filtered)
796 line = Sanitize(line) + "\n"
799 # Oops, something unspeakable happened.
805 def isRoleAccount(account):
806 return 'debianRoleAccount' in account['objectClass']
808 # Generate the DNS Zone file
809 def GenDNS(accounts, File):
812 F = open(File + ".tmp", "w")
814 # Fetch all the users
817 # Write out the zone file entry for each user
819 if not 'dnsZoneEntry' in a: continue
820 if not a.is_active_user() and not isRoleAccount(a): continue
821 if a.is_guest_account(): continue
824 F.write("; %s\n"%(a.email_address()))
825 for z in a["dnsZoneEntry"]:
826 Split = z.lower().split()
827 if Split[1].lower() == 'in':
828 Line = " ".join(Split) + "\n"
831 Host = Split[0] + DNSZone
832 if BSMTPCheck.match(Line) != None:
833 F.write("; Has BSMTP\n")
835 # Write some identification information
836 if not RRs.has_key(Host):
837 if Split[2].lower() in ["a", "aaaa"]:
838 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
839 for y in a["keyFingerPrint"]:
840 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
844 Line = "; Err %s"%(str(Split))
849 F.write("; Errors:\n")
850 for line in str(e).split("\n"):
851 F.write("; %s\n"%(line))
854 # Oops, something unspeakable happened.
862 socket.inet_pton(socket.AF_INET6, i)
867 def ExtractDNSInfo(x):
871 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
874 if x[1].has_key("ipHostNumber"):
875 for I in x[1]["ipHostNumber"]:
877 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
879 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
883 if 'sshRSAHostKey' in x[1]:
884 for I in x[1]["sshRSAHostKey"]:
886 if Split[0] == 'ssh-rsa':
888 if Split[0] == 'ssh-dss':
890 if Split[0] == 'ssh-ed25519':
892 if Algorithm == None:
894 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
895 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
896 Fingerprint = hashlib.new('sha256', base64.decodestring(Split[1])).hexdigest()
897 DNSInfo.append("%sIN\tSSHFP\t%u 2 %s" % (TTLprefix, Algorithm, Fingerprint))
899 if 'architecture' in x[1]:
900 Arch = GetAttr(x, "architecture")
902 if x[1].has_key("machine"):
903 Mach = " " + GetAttr(x, "machine")
904 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian"))
906 if x[1].has_key("mXRecord"):
907 for I in x[1]["mXRecord"]:
909 for e in MX_remap[I]:
910 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
912 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
916 # Generate the DNS records
917 def GenZoneRecords(host_attrs, File):
920 F = open(File + ".tmp", "w")
922 # Fetch all the hosts
924 if x[1].has_key("hostname") == 0:
927 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
930 DNSInfo = ExtractDNSInfo(x)
934 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
937 Line = "\t\t\t%s" % (Line)
941 # this would write sshfp lines for services on machines
942 # but we can't yet, since some are cnames and we'll make
943 # an invalid zonefile
945 # for i in x[1].get("purpose", []):
946 # m = PurposeHostField.match(i)
949 # # we ignore [[*..]] entries
950 # if m.startswith('*'):
952 # if m.startswith('-'):
955 # if not m.endswith(HostDomain):
957 # if not m.endswith('.'):
959 # for Line in DNSInfo:
960 # if isSSHFP.match(Line):
961 # Line = "%s\t%s" % (m, Line)
962 # F.write(Line + "\n")
964 # Oops, something unspeakable happened.
970 # Generate the BSMTP file
971 def GenBSMTP(accounts, File, HomePrefix):
974 F = open(File + ".tmp", "w")
976 # Write out the zone file entry for each user
978 if not 'dnsZoneEntry' in a: continue
979 if not a.is_active_user(): continue
982 for z in a["dnsZoneEntry"]:
983 Split = z.lower().split()
984 if Split[1].lower() == 'in':
985 for y in range(0, len(Split)):
988 Line = " ".join(Split) + "\n"
990 Host = Split[0] + DNSZone
991 if BSMTPCheck.match(Line) != None:
992 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
993 a['uid'], HomePrefix, a['uid'], Host))
996 F.write("; Errors\n")
999 # Oops, something unspeakable happened.
1005 def HostToIP(Host, mapped=True):
1009 if Host[1].has_key("ipHostNumber"):
1010 for addr in Host[1]["ipHostNumber"]:
1011 IPAdresses.append(addr)
1012 if not is_ipv6_addr(addr) and mapped == "True":
1013 IPAdresses.append("::ffff:"+addr)
1017 # Generate the ssh known hosts file
1018 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
1021 OldMask = os.umask(0022)
1022 F = open(File + ".tmp", "w", 0644)
1025 for x in host_attrs:
1026 if x[1].has_key("hostname") == 0 or \
1027 x[1].has_key("sshRSAHostKey") == 0:
1029 Host = GetAttr(x, "hostname")
1030 HostNames = [ Host ]
1031 if Host.endswith(HostDomain):
1032 HostNames.append(Host[:-(len(HostDomain) + 1)])
1034 # in the purpose field [[host|some other text]] (where some other text is optional)
1035 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1036 # file. But so that we don't have to add everything we link we can add an asterisk
1037 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
1038 # http linking it we also support [[-hostname]] entries.
1039 for i in x[1].get("purpose", []):
1040 m = PurposeHostField.match(i)
1043 # we ignore [[*..]] entries
1044 if m.startswith('*'):
1046 if m.startswith('-'):
1050 if m.endswith(HostDomain):
1051 HostNames.append(m[:-(len(HostDomain) + 1)])
1053 for I in x[1]["sshRSAHostKey"]:
1054 if mode and mode == 'authorized_keys':
1056 if 'sshdistAuthKeysHost' in x[1]:
1057 hosts += x[1]['sshdistAuthKeysHost']
1058 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1059 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1060 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1062 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1063 Line = Sanitize(Line) + "\n"
1065 # Oops, something unspeakable happened.
1071 # Generate the debianhosts file (list of all IP addresses)
1072 def GenHosts(host_attrs, File):
1075 OldMask = os.umask(0022)
1076 F = open(File + ".tmp", "w", 0644)
1081 for x in host_attrs:
1083 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1086 if not 'ipHostNumber' in x[1]:
1089 addrs = x[1]["ipHostNumber"]
1091 if addr not in seen:
1093 addr = Sanitize(addr) + "\n"
1096 # Oops, something unspeakable happened.
1102 def replaceTree(src, dst_basedir):
1103 bn = os.path.basename(src)
1104 dst = os.path.join(dst_basedir, bn)
1106 shutil.copytree(src, dst)
1108 def GenKeyrings(OutDir):
1110 if os.path.isdir(k):
1111 replaceTree(k, OutDir)
1113 shutil.copy(k, OutDir)
1116 def get_accounts(ldap_conn):
1117 # Fetch all the users
1118 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1119 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1120 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1121 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1122 "shadowExpire", "emailForward", "latitude", "longitude",\
1123 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1124 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1125 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1126 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1127 "mailContentInspectionAction", "webPassword", "rtcPassword",\
1130 if passwd_attrs is None:
1131 raise UDEmptyList, "No Users"
1132 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1133 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1137 def get_hosts(ldap_conn):
1138 # Fetch all the hosts
1139 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1140 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1141 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1143 if HostAttrs == None:
1144 raise UDEmptyList, "No Hosts"
1146 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1151 def make_ldap_conn():
1152 # Connect to the ldap server
1154 # for testing purposes it's sometimes useful to pass username/password
1155 # via the environment
1156 if 'UD_CREDENTIALS' in os.environ:
1157 Pass = os.environ['UD_CREDENTIALS'].split()
1159 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1160 Pass = F.readline().strip().split(" ")
1162 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1168 def setup_group_maps(l):
1169 # Fetch all the groups
1172 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1173 ["gid", "gidNumber", "subGroup"])
1175 # Generate the subgroup_map and group_id_map
1177 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1179 if x[1].has_key("gidNumber") == 0:
1181 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1182 if x[1].has_key("subGroup") != 0:
1183 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1187 SubGroupMap = subgroup_map
1188 GroupIDMap = group_id_map
1190 def generate_all(global_dir, ldap_conn):
1191 accounts = get_accounts(ldap_conn)
1192 host_attrs = get_hosts(ldap_conn)
1195 # Generate global things
1196 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1198 accounts = filter(lambda x: not IsRetired(x), accounts)
1200 CheckForward(accounts)
1202 GenMailDisable(accounts, global_dir + "mail-disable")
1203 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1204 GenDBM(accounts, global_dir + "mail-forward.db", 'emailForward')
1205 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1206 GenDBM(accounts, global_dir + "mail-contentinspectionaction.db", 'mailContentInspectionAction')
1207 GenPrivate(accounts, global_dir + "debian-private")
1208 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1209 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1210 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1211 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1212 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1213 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1214 GenWebPassword(accounts, global_dir + "web-passwords")
1215 GenRtcPassword(accounts, global_dir + "rtc-passwords")
1216 GenKeyrings(global_dir)
1219 GenForward(accounts, global_dir + "forward-alias")
1221 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1222 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1224 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1225 GenMarkers(accounts, global_dir + "markers")
1226 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1227 GenHosts(host_attrs, global_dir + "debianhosts")
1228 GenSSHGitolite(accounts, host_attrs, global_dir + "ssh-gitolite")
1230 GenDNS(accounts, global_dir + "dns-zone")
1231 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1233 setup_group_maps(ldap_conn)
1235 for host in host_attrs:
1236 if not "hostname" in host[1]:
1238 generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1240 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1241 current_host = host[1]['hostname'][0]
1242 OutDir = global_dir + current_host + '/'
1243 if not os.path.isdir(OutDir):
1246 # Get the group list and convert any named groups to numerics
1248 for groupname in AllowedGroupsPreload.strip().split(" "):
1249 GroupList[groupname] = True
1250 if 'allowedGroups' in host[1]:
1251 for groupname in host[1]['allowedGroups']:
1252 GroupList[groupname] = True
1253 for groupname in GroupList.keys():
1254 if groupname in GroupIDMap:
1255 GroupList[str(GroupIDMap[groupname])] = True
1258 if 'exportOptions' in host[1]:
1259 for extra in host[1]['exportOptions']:
1260 ExtraList[extra.upper()] = True
1263 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1265 DoLink(global_dir, OutDir, "debianhosts")
1266 DoLink(global_dir, OutDir, "ssh_known_hosts")
1267 DoLink(global_dir, OutDir, "disabled-accounts")
1270 if 'NOPASSWD' in ExtraList:
1271 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1273 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1275 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1276 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1278 # Now we know who we're allowing on the machine, export
1279 # the relevant ssh keys
1280 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1282 if not 'NOPASSWD' in ExtraList:
1283 GenShadow(accounts, OutDir + "shadow")
1285 # Link in global things
1286 if not 'NOMARKERS' in ExtraList:
1287 DoLink(global_dir, OutDir, "markers")
1288 DoLink(global_dir, OutDir, "mail-forward.cdb")
1289 DoLink(global_dir, OutDir, "mail-forward.db")
1290 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1291 DoLink(global_dir, OutDir, "mail-contentinspectionaction.db")
1292 DoLink(global_dir, OutDir, "mail-disable")
1293 DoLink(global_dir, OutDir, "mail-greylist")
1294 DoLink(global_dir, OutDir, "mail-callout")
1295 DoLink(global_dir, OutDir, "mail-rbl")
1296 DoLink(global_dir, OutDir, "mail-rhsbl")
1297 DoLink(global_dir, OutDir, "mail-whitelist")
1298 DoLink(global_dir, OutDir, "all-accounts.json")
1299 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1300 GenDBM(accounts, OutDir + "user-forward.db", 'emailForward')
1301 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1302 GenDBM(accounts, OutDir + "batv-tokens.db", 'bATVToken')
1303 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1304 GenDBM(accounts, OutDir + "default-mail-options.db", 'mailDefaultOptions')
1307 DoLink(global_dir, OutDir, "forward-alias")
1309 if 'DNS' in ExtraList:
1310 DoLink(global_dir, OutDir, "dns-zone")
1311 DoLink(global_dir, OutDir, "dns-sshfp")
1313 if 'AUTHKEYS' in ExtraList:
1314 DoLink(global_dir, OutDir, "authorized_keys")
1316 if 'BSMTP' in ExtraList:
1317 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1319 if 'PRIVATE' in ExtraList:
1320 DoLink(global_dir, OutDir, "debian-private")
1322 if 'GITOLITE' in ExtraList:
1323 DoLink(global_dir, OutDir, "ssh-gitolite")
1324 if 'exportOptions' in host[1]:
1325 for entry in host[1]['exportOptions']:
1326 v = entry.split('=',1)
1327 if v[0] != 'GITOLITE' or len(v) != 2: continue
1328 options = v[1].split(',')
1329 group = options.pop(0);
1330 gitolite_accounts = filter(lambda x: IsInGroup(x, [group], current_host), all_accounts)
1331 if not 'nohosts' in options:
1332 gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1337 if opt.startswith('sshcmd='):
1338 command = opt.split('=',1)[1]
1339 GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(group,), sshcommand=command, current_host=current_host)
1341 if 'WEB-PASSWORDS' in ExtraList:
1342 DoLink(global_dir, OutDir, "web-passwords")
1344 if 'RTC-PASSWORDS' in ExtraList:
1345 DoLink(global_dir, OutDir, "rtc-passwords")
1347 if 'KEYRING' in ExtraList:
1349 bn = os.path.basename(k)
1350 if os.path.isdir(k):
1351 src = os.path.join(global_dir, bn)
1352 replaceTree(src, OutDir)
1354 DoLink(global_dir, OutDir, bn)
1358 bn = os.path.basename(k)
1359 target = os.path.join(OutDir, bn)
1360 if os.path.isdir(target):
1363 posix.remove(target)
1366 DoLink(global_dir, OutDir, "last_update.trace")
1369 def getLastLDAPChangeTime(l):
1370 mods = l.search_s('cn=log',
1371 ldap.SCOPE_ONELEVEL,
1372 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1377 # Sort the list by reqEnd
1378 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1379 # Take the last element in the array
1380 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1384 def getLastKeyringChangeTime():
1387 mt = os.path.getmtime(k)
1393 def getLastBuildTime(gdir):
1394 cache_last_ldap_mod = 0
1395 cache_last_unix_mod = 0
1399 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1400 cache_last_mod=fd.read().split()
1402 cache_last_ldap_mod = cache_last_mod[0]
1403 cache_last_unix_mod = int(cache_last_mod[1])
1404 cache_last_run = int(cache_last_mod[2])
1405 except IndexError, ValueError:
1409 if e.errno == errno.ENOENT:
1414 return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1416 def mq_notify(options, message):
1417 options.section = 'dsa-udgenerate'
1418 options.config = '/etc/dsa/pubsub.conf'
1420 config = Config(options)
1422 'rabbit_userid': config.username,
1423 'rabbit_password': config.password,
1424 'rabbit_virtual_host': config.vhost,
1425 'rabbit_hosts': ['pubsub02.debian.org', 'pubsub01.debian.org'],
1431 'timestamp': int(time.time())
1435 conn = Connection(conf=conf)
1436 conn.topic_send(config.topic,
1438 exchange_name=config.exchange,
1445 parser = optparse.OptionParser()
1446 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1447 help="Output directory.")
1448 parser.add_option("-f", "--force", dest="force", action="store_true",
1449 help="Force generation, even if no update to LDAP has happened.")
1451 (options, args) = parser.parse_args()
1456 if options.generatedir is not None:
1457 generate_dir = os.environ['UD_GENERATEDIR']
1458 elif 'UD_GENERATEDIR' in os.environ:
1459 generate_dir = os.environ['UD_GENERATEDIR']
1461 generate_dir = GenerateDir
1464 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1465 lock = get_lock( lockf )
1467 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1470 l = make_ldap_conn()
1472 time_started = int(time.time())
1473 ldap_last_mod = getLastLDAPChangeTime(l)
1474 unix_last_mod = getLastKeyringChangeTime()
1475 cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1477 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)
1479 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1480 if need_update or options.force:
1481 msg = 'Update forced' if options.force else 'Update needed'
1482 generate_all(generate_dir, l)
1484 mq_notify(options, msg)
1485 last_run = int(time.time())
1486 fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1491 if __name__ == "__main__":
1492 if 'UD_PROFILE' in os.environ:
1495 cProfile.run('ud_generate()', "udg_prof")
1496 p = pstats.Stats('udg_prof')
1497 ##p.sort_stats('time').print_stats()
1498 p.sort_stats('cumulative').print_stats()
1504 # vim:set shiftwidth=3: