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 a.is_guest_account(): continue
441 if not 'rtcPassword' in a: continue
442 if not a.pw_active(): continue
444 Line = "%s%s:%s:%s:AUTHORIZED" % (a['uid'], rtc_append, str(a['rtcPassword']), rtc_realm)
445 Line = Sanitize(Line) + "\n"
446 F.write("%s" % (Line))
452 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
453 OldMask = os.umask(0077)
454 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
457 if f not in ssh_userkeys:
459 # If we're not exporting their primary group, don't export
462 if userlist[f] in grouprevmap.keys():
463 grname = grouprevmap[userlist[f]]
466 if int(userlist[f]) <= 100:
467 # In these cases, look it up in the normal way so we
468 # deal with cases where, for instance, users are in group
469 # users as their primary group.
470 grname = grp.getgrgid(userlist[f])[0]
475 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])
479 for line in ssh_userkeys[f]:
480 if line.startswith("allowed_hosts=") and ' ' in line:
481 machines, line = line.split('=', 1)[1].split(' ', 1)
482 if current_host not in machines.split(','):
483 continue # skip this key
486 continue # no keys for this host
487 contents = "\n".join(lines) + "\n"
489 to = tarfile.TarInfo(name=f)
490 # These will only be used where the username doesn't
491 # exist on the target system for some reason; hence,
492 # in those cases, the safest thing is for the file to
493 # be owned by root but group nobody. This deals with
494 # the bloody obscure case where the group fails to exist
495 # whilst the user does (in which case we want to avoid
496 # ending up with a file which is owned user:root to avoid
497 # a fairly obvious attack vector)
500 # Using the username / groupname fields avoids any need
501 # to give a shit^W^W^Wcare about the UIDoffset stuff.
505 to.mtime = int(time.time())
506 to.size = len(contents)
508 tf.addfile(to, StringIO(contents))
511 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
513 # add a list of groups to existing groups,
514 # including all subgroups thereof, recursively.
515 # basically this proceduces the transitive hull of the groups in
517 def addGroups(existingGroups, newGroups, uid, current_host):
518 for group in newGroups:
519 # if it's a <group>@host, split it and verify it's on the current host.
520 s = group.split('@', 1)
521 if len(s) == 2 and s[1] != current_host:
525 # let's see if we handled this group already
526 if group in existingGroups:
529 if not GroupIDMap.has_key(group):
530 print "Group", group, "does not exist but", uid, "is in it"
533 existingGroups.append(group)
535 if SubGroupMap.has_key(group):
536 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
538 # Generate the group list
539 def GenGroup(accounts, File, current_host):
543 F = open(File + ".tdb.tmp", "w")
545 # Generate the GroupMap
549 GroupHasPrimaryMembers = {}
551 # Sort them into a list of groups having a set of users
553 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
554 if not 'supplementaryGid' in a: continue
557 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
559 GroupMap[g].append(a['uid'])
561 # Output the group file.
563 for x in GroupMap.keys():
564 if not x in GroupIDMap:
567 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
570 grouprevmap[GroupIDMap[x]] = x
572 Line = "%s:x:%u:" % (x, GroupIDMap[x])
574 for I in GroupMap[x]:
575 Line = Line + ("%s%s" % (Comma, I))
577 Line = Sanitize(Line) + "\n"
578 F.write("0%u %s" % (J, Line))
579 F.write(".%s %s" % (x, Line))
580 F.write("=%u %s" % (GroupIDMap[x], Line))
583 # Oops, something unspeakable happened.
591 def CheckForward(accounts):
593 if not 'emailForward' in a: continue
597 # Do not allow people to try to buffer overflow busted parsers
598 if len(a['emailForward']) > 200: delete = True
599 # Check the forwarding address
600 elif EmailCheck.match(a['emailForward']) is None: delete = True
603 a.delete_mailforward()
605 # Generate the email forwarding list
606 def GenForward(accounts, File):
609 OldMask = os.umask(0022)
610 F = open(File + ".tmp", "w", 0644)
614 if not 'emailForward' in a: continue
615 Line = "%s: %s" % (a['uid'], a['emailForward'])
616 Line = Sanitize(Line) + "\n"
619 # Oops, something unspeakable happened.
625 def GenCDB(accounts, File, key):
628 OldMask = os.umask(0022)
629 # nothing else does the fsync stuff, so why do it here?
630 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
631 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
634 # Write out the email address for each user
636 if not key in a: continue
639 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
642 # Oops, something unspeakable happened.
646 if Fdb.close() != None:
647 raise "cdbmake gave an error"
649 def GenDBM(accounts, File, key):
651 OldMask = os.umask(0022)
652 fn = os.path.join(File).encode('ascii', 'ignore')
659 Fdb = dbm.open(fn, "c")
662 # Write out the email address for each user
664 if not key in a: continue
671 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
672 os.remove(File + ".db")
674 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
675 os.rename (File + ".db", File)
677 # Generate the anon XEarth marker file
678 def GenMarkers(accounts, File):
681 F = open(File + ".tmp", "w")
683 # Write out the position for each user
685 if not ('latitude' in a and 'longitude' in a): continue
687 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
688 Line = Sanitize(Line) + "\n"
693 # Oops, something unspeakable happened.
699 # Generate the debian-private subscription list
700 def GenPrivate(accounts, File):
703 F = open(File + ".tmp", "w")
705 # Write out the position for each user
707 if not a.is_active_user(): continue
708 if a.is_guest_account(): continue
709 if not 'privateSub' in a: continue
711 Line = "%s"%(a['privateSub'])
712 Line = Sanitize(Line) + "\n"
717 # Oops, something unspeakable happened.
723 # Generate a list of locked accounts
724 def GenDisabledAccounts(accounts, File):
727 F = open(File + ".tmp", "w")
728 disabled_accounts = []
730 # Fetch all the users
732 if a.pw_active(): continue
733 Line = "%s:%s" % (a['uid'], "Account is locked")
734 disabled_accounts.append(a)
735 F.write(Sanitize(Line) + "\n")
737 # Oops, something unspeakable happened.
742 return disabled_accounts
744 # Generate the list of local addresses that refuse all mail
745 def GenMailDisable(accounts, File):
748 F = open(File + ".tmp", "w")
751 if not 'mailDisableMessage' in a: continue
752 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
753 Line = Sanitize(Line) + "\n"
756 # Oops, something unspeakable happened.
762 # Generate a list of uids that should have boolean affects applied
763 def GenMailBool(accounts, File, key):
766 F = open(File + ".tmp", "w")
769 if not key in a: continue
770 if not a[key] == 'TRUE': continue
771 Line = "%s"%(a['uid'])
772 Line = Sanitize(Line) + "\n"
775 # Oops, something unspeakable happened.
781 # Generate a list of hosts for RBL or whitelist purposes.
782 def GenMailList(accounts, File, key):
785 F = open(File + ".tmp", "w")
787 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
788 else: validregex = re.compile('^[-\w.]+$')
791 if not key in a: continue
793 filtered = filter(lambda z: validregex.match(z), a[key])
794 if len(filtered) == 0: continue
795 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
796 line = a['uid'] + ': ' + ' : '.join(filtered)
797 line = Sanitize(line) + "\n"
800 # Oops, something unspeakable happened.
806 def isRoleAccount(account):
807 return 'debianRoleAccount' in account['objectClass']
809 # Generate the DNS Zone file
810 def GenDNS(accounts, File):
813 F = open(File + ".tmp", "w")
815 # Fetch all the users
818 # Write out the zone file entry for each user
820 if not 'dnsZoneEntry' in a: continue
821 if not a.is_active_user() and not isRoleAccount(a): continue
822 if a.is_guest_account(): continue
825 F.write("; %s\n"%(a.email_address()))
826 for z in a["dnsZoneEntry"]:
827 Split = z.lower().split()
828 if Split[1].lower() == 'in':
829 Line = " ".join(Split) + "\n"
832 Host = Split[0] + DNSZone
833 if BSMTPCheck.match(Line) != None:
834 F.write("; Has BSMTP\n")
836 # Write some identification information
837 if not RRs.has_key(Host):
838 if Split[2].lower() in ["a", "aaaa"]:
839 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
840 for y in a["keyFingerPrint"]:
841 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
845 Line = "; Err %s"%(str(Split))
850 F.write("; Errors:\n")
851 for line in str(e).split("\n"):
852 F.write("; %s\n"%(line))
855 # Oops, something unspeakable happened.
863 socket.inet_pton(socket.AF_INET6, i)
868 def ExtractDNSInfo(x):
872 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
875 if x[1].has_key("ipHostNumber"):
876 for I in x[1]["ipHostNumber"]:
878 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
880 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
884 if 'sshRSAHostKey' in x[1]:
885 for I in x[1]["sshRSAHostKey"]:
887 if Split[0] == 'ssh-rsa':
889 if Split[0] == 'ssh-dss':
891 if Split[0] == 'ssh-ed25519':
893 if Algorithm == None:
895 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
896 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
897 Fingerprint = hashlib.new('sha256', base64.decodestring(Split[1])).hexdigest()
898 DNSInfo.append("%sIN\tSSHFP\t%u 2 %s" % (TTLprefix, Algorithm, Fingerprint))
900 if 'architecture' in x[1]:
901 Arch = GetAttr(x, "architecture")
903 if x[1].has_key("machine"):
904 Mach = " " + GetAttr(x, "machine")
905 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian"))
907 if x[1].has_key("mXRecord"):
908 for I in x[1]["mXRecord"]:
910 for e in MX_remap[I]:
911 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
913 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
917 # Generate the DNS records
918 def GenZoneRecords(host_attrs, File):
921 F = open(File + ".tmp", "w")
923 # Fetch all the hosts
925 if x[1].has_key("hostname") == 0:
928 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
931 DNSInfo = ExtractDNSInfo(x)
935 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
938 Line = "\t\t\t%s" % (Line)
942 # this would write sshfp lines for services on machines
943 # but we can't yet, since some are cnames and we'll make
944 # an invalid zonefile
946 # for i in x[1].get("purpose", []):
947 # m = PurposeHostField.match(i)
950 # # we ignore [[*..]] entries
951 # if m.startswith('*'):
953 # if m.startswith('-'):
956 # if not m.endswith(HostDomain):
958 # if not m.endswith('.'):
960 # for Line in DNSInfo:
961 # if isSSHFP.match(Line):
962 # Line = "%s\t%s" % (m, Line)
963 # F.write(Line + "\n")
965 # Oops, something unspeakable happened.
971 # Generate the BSMTP file
972 def GenBSMTP(accounts, File, HomePrefix):
975 F = open(File + ".tmp", "w")
977 # Write out the zone file entry for each user
979 if not 'dnsZoneEntry' in a: continue
980 if not a.is_active_user(): continue
983 for z in a["dnsZoneEntry"]:
984 Split = z.lower().split()
985 if Split[1].lower() == 'in':
986 for y in range(0, len(Split)):
989 Line = " ".join(Split) + "\n"
991 Host = Split[0] + DNSZone
992 if BSMTPCheck.match(Line) != None:
993 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
994 a['uid'], HomePrefix, a['uid'], Host))
997 F.write("; Errors\n")
1000 # Oops, something unspeakable happened.
1006 def HostToIP(Host, mapped=True):
1010 if Host[1].has_key("ipHostNumber"):
1011 for addr in Host[1]["ipHostNumber"]:
1012 IPAdresses.append(addr)
1013 if not is_ipv6_addr(addr) and mapped == "True":
1014 IPAdresses.append("::ffff:"+addr)
1018 # Generate the ssh known hosts file
1019 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
1022 OldMask = os.umask(0022)
1023 F = open(File + ".tmp", "w", 0644)
1026 for x in host_attrs:
1027 if x[1].has_key("hostname") == 0 or \
1028 x[1].has_key("sshRSAHostKey") == 0:
1030 Host = GetAttr(x, "hostname")
1031 HostNames = [ Host ]
1032 if Host.endswith(HostDomain):
1033 HostNames.append(Host[:-(len(HostDomain) + 1)])
1035 # in the purpose field [[host|some other text]] (where some other text is optional)
1036 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1037 # file. But so that we don't have to add everything we link we can add an asterisk
1038 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
1039 # http linking it we also support [[-hostname]] entries.
1040 for i in x[1].get("purpose", []):
1041 m = PurposeHostField.match(i)
1044 # we ignore [[*..]] entries
1045 if m.startswith('*'):
1047 if m.startswith('-'):
1051 if m.endswith(HostDomain):
1052 HostNames.append(m[:-(len(HostDomain) + 1)])
1054 for I in x[1]["sshRSAHostKey"]:
1055 if mode and mode == 'authorized_keys':
1057 if 'sshdistAuthKeysHost' in x[1]:
1058 hosts += x[1]['sshdistAuthKeysHost']
1059 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1060 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1061 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1063 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1064 Line = Sanitize(Line) + "\n"
1066 # Oops, something unspeakable happened.
1072 # Generate the debianhosts file (list of all IP addresses)
1073 def GenHosts(host_attrs, File):
1076 OldMask = os.umask(0022)
1077 F = open(File + ".tmp", "w", 0644)
1082 for x in host_attrs:
1084 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1087 if not 'ipHostNumber' in x[1]:
1090 addrs = x[1]["ipHostNumber"]
1092 if addr not in seen:
1094 addr = Sanitize(addr) + "\n"
1097 # Oops, something unspeakable happened.
1103 def replaceTree(src, dst_basedir):
1104 bn = os.path.basename(src)
1105 dst = os.path.join(dst_basedir, bn)
1107 shutil.copytree(src, dst)
1109 def GenKeyrings(OutDir):
1111 if os.path.isdir(k):
1112 replaceTree(k, OutDir)
1114 shutil.copy(k, OutDir)
1117 def get_accounts(ldap_conn):
1118 # Fetch all the users
1119 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1120 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1121 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1122 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1123 "shadowExpire", "emailForward", "latitude", "longitude",\
1124 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1125 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1126 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1127 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1128 "mailContentInspectionAction", "webPassword", "rtcPassword",\
1131 if passwd_attrs is None:
1132 raise UDEmptyList, "No Users"
1133 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1134 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1138 def get_hosts(ldap_conn):
1139 # Fetch all the hosts
1140 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1141 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1142 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1144 if HostAttrs == None:
1145 raise UDEmptyList, "No Hosts"
1147 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1152 def make_ldap_conn():
1153 # Connect to the ldap server
1155 # for testing purposes it's sometimes useful to pass username/password
1156 # via the environment
1157 if 'UD_CREDENTIALS' in os.environ:
1158 Pass = os.environ['UD_CREDENTIALS'].split()
1160 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1161 Pass = F.readline().strip().split(" ")
1163 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1169 def setup_group_maps(l):
1170 # Fetch all the groups
1173 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1174 ["gid", "gidNumber", "subGroup"])
1176 # Generate the subgroup_map and group_id_map
1178 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1180 if x[1].has_key("gidNumber") == 0:
1182 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1183 if x[1].has_key("subGroup") != 0:
1184 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1188 SubGroupMap = subgroup_map
1189 GroupIDMap = group_id_map
1191 def generate_all(global_dir, ldap_conn):
1192 accounts = get_accounts(ldap_conn)
1193 host_attrs = get_hosts(ldap_conn)
1196 # Generate global things
1197 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1199 accounts = filter(lambda x: not IsRetired(x), accounts)
1201 CheckForward(accounts)
1203 GenMailDisable(accounts, global_dir + "mail-disable")
1204 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1205 GenDBM(accounts, global_dir + "mail-forward.db", 'emailForward')
1206 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1207 GenDBM(accounts, global_dir + "mail-contentinspectionaction.db", 'mailContentInspectionAction')
1208 GenPrivate(accounts, global_dir + "debian-private")
1209 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1210 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1211 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1212 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1213 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1214 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1215 GenWebPassword(accounts, global_dir + "web-passwords")
1216 GenRtcPassword(accounts, global_dir + "rtc-passwords")
1217 GenKeyrings(global_dir)
1220 GenForward(accounts, global_dir + "forward-alias")
1222 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1223 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1225 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1226 GenMarkers(accounts, global_dir + "markers")
1227 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1228 GenHosts(host_attrs, global_dir + "debianhosts")
1229 GenSSHGitolite(accounts, host_attrs, global_dir + "ssh-gitolite")
1231 GenDNS(accounts, global_dir + "dns-zone")
1232 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1234 setup_group_maps(ldap_conn)
1236 for host in host_attrs:
1237 if not "hostname" in host[1]:
1239 generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1241 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1242 current_host = host[1]['hostname'][0]
1243 OutDir = global_dir + current_host + '/'
1244 if not os.path.isdir(OutDir):
1247 # Get the group list and convert any named groups to numerics
1249 for groupname in AllowedGroupsPreload.strip().split(" "):
1250 GroupList[groupname] = True
1251 if 'allowedGroups' in host[1]:
1252 for groupname in host[1]['allowedGroups']:
1253 GroupList[groupname] = True
1254 for groupname in GroupList.keys():
1255 if groupname in GroupIDMap:
1256 GroupList[str(GroupIDMap[groupname])] = True
1259 if 'exportOptions' in host[1]:
1260 for extra in host[1]['exportOptions']:
1261 ExtraList[extra.upper()] = True
1264 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1266 DoLink(global_dir, OutDir, "debianhosts")
1267 DoLink(global_dir, OutDir, "ssh_known_hosts")
1268 DoLink(global_dir, OutDir, "disabled-accounts")
1271 if 'NOPASSWD' in ExtraList:
1272 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1274 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1276 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1277 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1279 # Now we know who we're allowing on the machine, export
1280 # the relevant ssh keys
1281 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1283 if not 'NOPASSWD' in ExtraList:
1284 GenShadow(accounts, OutDir + "shadow")
1286 # Link in global things
1287 if not 'NOMARKERS' in ExtraList:
1288 DoLink(global_dir, OutDir, "markers")
1289 DoLink(global_dir, OutDir, "mail-forward.cdb")
1290 DoLink(global_dir, OutDir, "mail-forward.db")
1291 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1292 DoLink(global_dir, OutDir, "mail-contentinspectionaction.db")
1293 DoLink(global_dir, OutDir, "mail-disable")
1294 DoLink(global_dir, OutDir, "mail-greylist")
1295 DoLink(global_dir, OutDir, "mail-callout")
1296 DoLink(global_dir, OutDir, "mail-rbl")
1297 DoLink(global_dir, OutDir, "mail-rhsbl")
1298 DoLink(global_dir, OutDir, "mail-whitelist")
1299 DoLink(global_dir, OutDir, "all-accounts.json")
1300 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1301 GenDBM(accounts, OutDir + "user-forward.db", 'emailForward')
1302 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1303 GenDBM(accounts, OutDir + "batv-tokens.db", 'bATVToken')
1304 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1305 GenDBM(accounts, OutDir + "default-mail-options.db", 'mailDefaultOptions')
1308 DoLink(global_dir, OutDir, "forward-alias")
1310 if 'DNS' in ExtraList:
1311 DoLink(global_dir, OutDir, "dns-zone")
1312 DoLink(global_dir, OutDir, "dns-sshfp")
1314 if 'AUTHKEYS' in ExtraList:
1315 DoLink(global_dir, OutDir, "authorized_keys")
1317 if 'BSMTP' in ExtraList:
1318 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1320 if 'PRIVATE' in ExtraList:
1321 DoLink(global_dir, OutDir, "debian-private")
1323 if 'GITOLITE' in ExtraList:
1324 DoLink(global_dir, OutDir, "ssh-gitolite")
1325 if 'exportOptions' in host[1]:
1326 for entry in host[1]['exportOptions']:
1327 v = entry.split('=',1)
1328 if v[0] != 'GITOLITE' or len(v) != 2: continue
1329 options = v[1].split(',')
1330 group = options.pop(0);
1331 gitolite_accounts = filter(lambda x: IsInGroup(x, [group], current_host), all_accounts)
1332 if not 'nohosts' in options:
1333 gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1338 if opt.startswith('sshcmd='):
1339 command = opt.split('=',1)[1]
1340 GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(group,), sshcommand=command, current_host=current_host)
1342 if 'WEB-PASSWORDS' in ExtraList:
1343 DoLink(global_dir, OutDir, "web-passwords")
1345 if 'RTC-PASSWORDS' in ExtraList:
1346 DoLink(global_dir, OutDir, "rtc-passwords")
1348 if 'KEYRING' in ExtraList:
1350 bn = os.path.basename(k)
1351 if os.path.isdir(k):
1352 src = os.path.join(global_dir, bn)
1353 replaceTree(src, OutDir)
1355 DoLink(global_dir, OutDir, bn)
1359 bn = os.path.basename(k)
1360 target = os.path.join(OutDir, bn)
1361 if os.path.isdir(target):
1364 posix.remove(target)
1367 DoLink(global_dir, OutDir, "last_update.trace")
1370 def getLastLDAPChangeTime(l):
1371 mods = l.search_s('cn=log',
1372 ldap.SCOPE_ONELEVEL,
1373 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1378 # Sort the list by reqEnd
1379 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1380 # Take the last element in the array
1381 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1385 def getLastKeyringChangeTime():
1388 mt = os.path.getmtime(k)
1394 def getLastBuildTime(gdir):
1395 cache_last_ldap_mod = 0
1396 cache_last_unix_mod = 0
1400 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1401 cache_last_mod=fd.read().split()
1403 cache_last_ldap_mod = cache_last_mod[0]
1404 cache_last_unix_mod = int(cache_last_mod[1])
1405 cache_last_run = int(cache_last_mod[2])
1406 except IndexError, ValueError:
1410 if e.errno == errno.ENOENT:
1415 return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1417 def mq_notify(options, message):
1418 options.section = 'dsa-udgenerate'
1419 options.config = '/etc/dsa/pubsub.conf'
1421 config = Config(options)
1423 'rabbit_userid': config.username,
1424 'rabbit_password': config.password,
1425 'rabbit_virtual_host': config.vhost,
1426 'rabbit_hosts': ['pubsub02.debian.org', 'pubsub01.debian.org'],
1432 'timestamp': int(time.time())
1436 conn = Connection(conf=conf)
1437 conn.topic_send(config.topic,
1439 exchange_name=config.exchange,
1446 parser = optparse.OptionParser()
1447 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1448 help="Output directory.")
1449 parser.add_option("-f", "--force", dest="force", action="store_true",
1450 help="Force generation, even if no update to LDAP has happened.")
1452 (options, args) = parser.parse_args()
1457 if options.generatedir is not None:
1458 generate_dir = os.environ['UD_GENERATEDIR']
1459 elif 'UD_GENERATEDIR' in os.environ:
1460 generate_dir = os.environ['UD_GENERATEDIR']
1462 generate_dir = GenerateDir
1465 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1466 lock = get_lock( lockf )
1468 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1471 l = make_ldap_conn()
1473 time_started = int(time.time())
1474 ldap_last_mod = getLastLDAPChangeTime(l)
1475 unix_last_mod = getLastKeyringChangeTime()
1476 cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1478 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)
1480 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1481 if need_update or options.force:
1482 msg = 'Update forced' if options.force else 'Update needed'
1483 generate_all(generate_dir, l)
1485 mq_notify(options, msg)
1486 last_run = int(time.time())
1487 fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1492 if __name__ == "__main__":
1493 if 'UD_PROFILE' in os.environ:
1496 cProfile.run('ud_generate()', "udg_prof")
1497 p = pstats.Stats('udg_prof')
1498 ##p.sort_stats('time').print_stats()
1499 p.sort_stats('cumulative').print_stats()
1505 # vim:set shiftwidth=3: