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)
81 rtc_realm = getattr(ConfModule, "rtc_realm", None)
82 rtc_append = getattr(ConfModule, "rtc_append", None)
85 """Return a pretty-printed XML string for the Element.
87 rough_string = ElementTree.tostring(elem, 'utf-8')
88 reparsed = minidom.parseString(rough_string)
89 return reparsed.toprettyxml(indent=" ")
91 def safe_makedirs(dir):
95 if e.errno == errno.EEXIST:
100 def safe_rmtree(dir):
104 if e.errno == errno.ENOENT:
109 def get_lock(fn, wait=5*60):
112 ends = time.time() + wait
117 fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
121 if time.time() >= ends:
123 sl = min(sl*2, 10, ends - time.time())
129 return Str.translate(string.maketrans("\n\r\t", "$$$"))
131 def DoLink(From, To, File):
133 posix.remove(To + File)
136 posix.link(From + File, To + File)
138 def IsRetired(account):
140 Looks for accountStatus in the LDAP record and tries to
141 match it against one of the known retired statuses
144 status = account['accountStatus']
146 line = status.split()
149 if status == "inactive":
152 elif status == "memorial":
155 elif status == "retiring":
156 # We'll give them a few extra days over what we said
157 age = 6 * 31 * 24 * 60 * 60
159 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
167 # See if this user is in the group list
168 def IsInGroup(account, allowed, current_host):
169 # See if the primary group is in the list
170 if str(account['gidNumber']) in allowed: return True
172 # Check the host based ACL
173 if account.is_allowed_by_hostacl(current_host): return True
175 # See if there are supplementary groups
176 if not 'supplementaryGid' in account: return False
179 addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
185 def Die(File, F, Fdb):
191 os.remove(File + ".tmp")
195 os.remove(File + ".tdb.tmp")
199 def Done(File, F, Fdb):
202 os.rename(File + ".tmp", File)
205 os.rename(File + ".tdb.tmp", File + ".tdb")
207 # Generate the password list
208 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
211 F = open(File + ".tdb.tmp", "w")
216 # Do not let people try to buffer overflow some busted passwd parser.
217 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
219 userlist[a['uid']] = a['gidNumber']
220 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
226 HomePrefix, a['uid'],
228 line = Sanitize(line) + "\n"
229 F.write("0%u %s" % (i, line))
230 F.write(".%s %s" % (a['uid'], line))
231 F.write("=%d %s" % (a['uidNumber'], line))
234 # Oops, something unspeakable happened.
240 # Return the list of users so we know which keys to export
243 def GenAllUsers(accounts, file):
246 OldMask = os.umask(0022)
247 f = open(file + ".tmp", "w", 0644)
252 all.append( { 'uid': a['uid'],
253 'uidNumber': a['uidNumber'],
254 'active': a.pw_active() and a.shadow_active() } )
257 # Oops, something unspeakable happened.
263 # Generate the shadow list
264 def GenShadow(accounts, File):
267 OldMask = os.umask(0077)
268 F = open(File + ".tdb.tmp", "w", 0600)
273 # If the account is locked, mark it as such in shadow
274 # See Debian Bug #308229 for why we set it to 1 instead of 0
275 if not a.pw_active(): ShadowExpire = '1'
276 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
277 else: ShadowExpire = ''
280 values.append(a['uid'])
281 values.append(a.get_password())
282 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
283 if key in a: values.append(a[key])
284 else: values.append('')
285 values.append(ShadowExpire)
286 line = ':'.join(values)+':'
287 line = Sanitize(line) + "\n"
288 F.write("0%u %s" % (i, line))
289 F.write(".%s %s" % (a['uid'], line))
292 # Oops, something unspeakable happened.
298 # Generate the sudo passwd file
299 def GenShadowSudo(accounts, File, untrusted, current_host):
302 OldMask = os.umask(0077)
303 F = open(File + ".tmp", "w", 0600)
308 if 'sudoPassword' in a:
309 for entry in a['sudoPassword']:
310 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*-]+) ([^ ]+)$').match(entry)
313 uuid = Match.group(1)
314 status = Match.group(2)
315 hosts = Match.group(3)
316 cryptedpass = Match.group(4)
318 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
320 for_all = hosts == "*"
321 for_this_host = current_host in hosts.split(',')
322 if not (for_all or for_this_host):
324 # ignore * passwords for untrusted hosts, but copy host specific passwords
325 if for_all and untrusted:
328 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
333 Line = "%s:%s" % (a['uid'], Pass)
334 Line = Sanitize(Line) + "\n"
335 F.write("%s" % (Line))
337 # Oops, something unspeakable happened.
343 # Generate the sudo passwd file
344 def GenSSHGitolite(accounts, hosts, File, sshcommand=None, current_host=None):
346 if sshcommand is None:
347 sshcommand = GitoliteSSHCommand
349 OldMask = os.umask(0022)
350 F = open(File + ".tmp", "w", 0600)
353 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
355 if not 'sshRSAAuthKey' in a: continue
358 prefix = GitoliteSSHRestrictions
359 prefix = prefix.replace('@@COMMAND@@', sshcommand)
360 prefix = prefix.replace('@@USER@@', User)
361 for I in a["sshRSAAuthKey"]:
362 if I.startswith("allowed_hosts=") and ' ' in line:
363 if current_host is None:
365 machines, I = I.split('=', 1)[1].split(' ', 1)
366 if current_host not in machines.split(','):
367 continue # skip this key
369 if I.startswith('ssh-'):
370 line = "%s %s"%(prefix, I)
372 continue # do not allow keys with other restrictions that might conflict
373 line = Sanitize(line) + "\n"
376 for dn, attrs in hosts:
377 if not 'sshRSAHostKey' in attrs: continue
378 hostname = "host-" + attrs['hostname'][0]
379 prefix = GitoliteSSHRestrictions
380 prefix = prefix.replace('@@COMMAND@@', sshcommand)
381 prefix = prefix.replace('@@USER@@', hostname)
382 for I in attrs["sshRSAHostKey"]:
383 line = "%s %s"%(prefix, I)
384 line = Sanitize(line) + "\n"
387 # Oops, something unspeakable happened.
393 # Generate the shadow list
394 def GenSSHShadow(global_dir, accounts):
395 # Fetch all the users
399 if not 'sshRSAAuthKey' in a: continue
402 for I in a['sshRSAAuthKey']:
403 MultipleLine = "%s" % I
404 MultipleLine = Sanitize(MultipleLine)
405 contents.append(MultipleLine)
406 userkeys[a['uid']] = contents
409 # Generate the webPassword list
410 def GenWebPassword(accounts, File):
413 OldMask = os.umask(0077)
414 F = open(File, "w", 0600)
418 if not 'webPassword' in a: continue
419 if not a.pw_active(): continue
421 Pass = str(a['webPassword'])
422 Line = "%s:%s" % (a['uid'], Pass)
423 Line = Sanitize(Line) + "\n"
424 F.write("%s" % (Line))
430 # Generate the rtcPassword list
431 def GenRtcPassword(accounts, File):
434 OldMask = os.umask(0077)
435 F = open(File, "w", 0600)
439 if not 'rtcPassword' in a: continue
440 if not a.pw_active(): continue
442 Line = "%s%s:%s:%s:AUTHORIZED" % (a['uid'], rtc_append, str(a['rtcPassword']), rtc_realm)
443 Line = Sanitize(Line) + "\n"
444 F.write("%s" % (Line))
450 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
451 OldMask = os.umask(0077)
452 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
455 if f not in ssh_userkeys:
457 # If we're not exporting their primary group, don't export
460 if userlist[f] in grouprevmap.keys():
461 grname = grouprevmap[userlist[f]]
464 if int(userlist[f]) <= 100:
465 # In these cases, look it up in the normal way so we
466 # deal with cases where, for instance, users are in group
467 # users as their primary group.
468 grname = grp.getgrgid(userlist[f])[0]
473 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])
477 for line in ssh_userkeys[f]:
478 if line.startswith("allowed_hosts=") and ' ' in line:
479 machines, line = line.split('=', 1)[1].split(' ', 1)
480 if current_host not in machines.split(','):
481 continue # skip this key
484 continue # no keys for this host
485 contents = "\n".join(lines) + "\n"
487 to = tarfile.TarInfo(name=f)
488 # These will only be used where the username doesn't
489 # exist on the target system for some reason; hence,
490 # in those cases, the safest thing is for the file to
491 # be owned by root but group nobody. This deals with
492 # the bloody obscure case where the group fails to exist
493 # whilst the user does (in which case we want to avoid
494 # ending up with a file which is owned user:root to avoid
495 # a fairly obvious attack vector)
498 # Using the username / groupname fields avoids any need
499 # to give a shit^W^W^Wcare about the UIDoffset stuff.
503 to.mtime = int(time.time())
504 to.size = len(contents)
506 tf.addfile(to, StringIO(contents))
509 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
511 # add a list of groups to existing groups,
512 # including all subgroups thereof, recursively.
513 # basically this proceduces the transitive hull of the groups in
515 def addGroups(existingGroups, newGroups, uid, current_host):
516 for group in newGroups:
517 # if it's a <group>@host, split it and verify it's on the current host.
518 s = group.split('@', 1)
519 if len(s) == 2 and s[1] != current_host:
523 # let's see if we handled this group already
524 if group in existingGroups:
527 if not GroupIDMap.has_key(group):
528 print "Group", group, "does not exist but", uid, "is in it"
531 existingGroups.append(group)
533 if SubGroupMap.has_key(group):
534 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
536 # Generate the group list
537 def GenGroup(accounts, File, current_host):
541 F = open(File + ".tdb.tmp", "w")
543 # Generate the GroupMap
547 GroupHasPrimaryMembers = {}
549 # Sort them into a list of groups having a set of users
551 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
552 if not 'supplementaryGid' in a: continue
555 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
557 GroupMap[g].append(a['uid'])
559 # Output the group file.
561 for x in GroupMap.keys():
562 if not x in GroupIDMap:
565 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
568 grouprevmap[GroupIDMap[x]] = x
570 Line = "%s:x:%u:" % (x, GroupIDMap[x])
572 for I in GroupMap[x]:
573 Line = Line + ("%s%s" % (Comma, I))
575 Line = Sanitize(Line) + "\n"
576 F.write("0%u %s" % (J, Line))
577 F.write(".%s %s" % (x, Line))
578 F.write("=%u %s" % (GroupIDMap[x], Line))
581 # Oops, something unspeakable happened.
589 def CheckForward(accounts):
591 if not 'emailForward' in a: continue
595 # Do not allow people to try to buffer overflow busted parsers
596 if len(a['emailForward']) > 200: delete = True
597 # Check the forwarding address
598 elif EmailCheck.match(a['emailForward']) is None: delete = True
601 a.delete_mailforward()
603 # Generate the email forwarding list
604 def GenForward(accounts, File):
607 OldMask = os.umask(0022)
608 F = open(File + ".tmp", "w", 0644)
612 if not 'emailForward' in a: continue
613 Line = "%s: %s" % (a['uid'], a['emailForward'])
614 Line = Sanitize(Line) + "\n"
617 # Oops, something unspeakable happened.
623 def GenCDB(accounts, File, key):
626 OldMask = os.umask(0022)
627 # nothing else does the fsync stuff, so why do it here?
628 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
629 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
632 # Write out the email address for each user
634 if not key in a: continue
637 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
640 # Oops, something unspeakable happened.
644 if Fdb.close() != None:
645 raise "cdbmake gave an error"
647 def GenDBM(accounts, File, key):
649 OldMask = os.umask(0022)
650 fn = os.path.join(File).encode('ascii', 'ignore')
657 Fdb = dbm.open(fn, "c")
660 # Write out the email address for each user
662 if not key in a: continue
669 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
670 os.remove(File + ".db")
672 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
673 os.rename (File + ".db", File)
675 # Generate the anon XEarth marker file
676 def GenMarkers(accounts, File):
679 F = open(File + ".tmp", "w")
681 # Write out the position for each user
683 if not ('latitude' in a and 'longitude' in a): continue
685 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
686 Line = Sanitize(Line) + "\n"
691 # Oops, something unspeakable happened.
697 # Generate the debian-private subscription list
698 def GenPrivate(accounts, File):
701 F = open(File + ".tmp", "w")
703 # Write out the position for each user
705 if not a.is_active_user(): continue
706 if a.is_guest_account(): continue
707 if not 'privateSub' in a: continue
709 Line = "%s"%(a['privateSub'])
710 Line = Sanitize(Line) + "\n"
715 # Oops, something unspeakable happened.
721 # Generate a list of locked accounts
722 def GenDisabledAccounts(accounts, File):
725 F = open(File + ".tmp", "w")
726 disabled_accounts = []
728 # Fetch all the users
730 if a.pw_active(): continue
731 Line = "%s:%s" % (a['uid'], "Account is locked")
732 disabled_accounts.append(a)
733 F.write(Sanitize(Line) + "\n")
735 # Oops, something unspeakable happened.
740 return disabled_accounts
742 # Generate the list of local addresses that refuse all mail
743 def GenMailDisable(accounts, File):
746 F = open(File + ".tmp", "w")
749 if not 'mailDisableMessage' in a: continue
750 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
751 Line = Sanitize(Line) + "\n"
754 # Oops, something unspeakable happened.
760 # Generate a list of uids that should have boolean affects applied
761 def GenMailBool(accounts, File, key):
764 F = open(File + ".tmp", "w")
767 if not key in a: continue
768 if not a[key] == 'TRUE': continue
769 Line = "%s"%(a['uid'])
770 Line = Sanitize(Line) + "\n"
773 # Oops, something unspeakable happened.
779 # Generate a list of hosts for RBL or whitelist purposes.
780 def GenMailList(accounts, File, key):
783 F = open(File + ".tmp", "w")
785 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
786 else: validregex = re.compile('^[-\w.]+$')
789 if not key in a: continue
791 filtered = filter(lambda z: validregex.match(z), a[key])
792 if len(filtered) == 0: continue
793 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
794 line = a['uid'] + ': ' + ' : '.join(filtered)
795 line = Sanitize(line) + "\n"
798 # Oops, something unspeakable happened.
804 def isRoleAccount(account):
805 return 'debianRoleAccount' in account['objectClass']
807 # Generate the DNS Zone file
808 def GenDNS(accounts, File):
811 F = open(File + ".tmp", "w")
813 # Fetch all the users
816 # Write out the zone file entry for each user
818 if not 'dnsZoneEntry' in a: continue
819 if not a.is_active_user() and not isRoleAccount(a): continue
820 if a.is_guest_account(): continue
823 F.write("; %s\n"%(a.email_address()))
824 for z in a["dnsZoneEntry"]:
825 Split = z.lower().split()
826 if Split[1].lower() == 'in':
827 Line = " ".join(Split) + "\n"
830 Host = Split[0] + DNSZone
831 if BSMTPCheck.match(Line) != None:
832 F.write("; Has BSMTP\n")
834 # Write some identification information
835 if not RRs.has_key(Host):
836 if Split[2].lower() in ["a", "aaaa"]:
837 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
838 for y in a["keyFingerPrint"]:
839 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
843 Line = "; Err %s"%(str(Split))
848 F.write("; Errors:\n")
849 for line in str(e).split("\n"):
850 F.write("; %s\n"%(line))
853 # Oops, something unspeakable happened.
861 socket.inet_pton(socket.AF_INET6, i)
866 def ExtractDNSInfo(x):
870 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
873 if x[1].has_key("ipHostNumber"):
874 for I in x[1]["ipHostNumber"]:
876 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
878 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
882 if 'sshRSAHostKey' in x[1]:
883 for I in x[1]["sshRSAHostKey"]:
885 if Split[0] == 'ssh-rsa':
887 if Split[0] == 'ssh-dss':
889 if Split[0] == 'ssh-ed25519':
891 if Algorithm == None:
893 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
894 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
895 Fingerprint = hashlib.new('sha256', base64.decodestring(Split[1])).hexdigest()
896 DNSInfo.append("%sIN\tSSHFP\t%u 2 %s" % (TTLprefix, Algorithm, Fingerprint))
898 if 'architecture' in x[1]:
899 Arch = GetAttr(x, "architecture")
901 if x[1].has_key("machine"):
902 Mach = " " + GetAttr(x, "machine")
903 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian"))
905 if x[1].has_key("mXRecord"):
906 for I in x[1]["mXRecord"]:
908 for e in MX_remap[I]:
909 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
911 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
915 # Generate the DNS records
916 def GenZoneRecords(host_attrs, File):
919 F = open(File + ".tmp", "w")
921 # Fetch all the hosts
923 if x[1].has_key("hostname") == 0:
926 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
929 DNSInfo = ExtractDNSInfo(x)
933 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
936 Line = "\t\t\t%s" % (Line)
940 # this would write sshfp lines for services on machines
941 # but we can't yet, since some are cnames and we'll make
942 # an invalid zonefile
944 # for i in x[1].get("purpose", []):
945 # m = PurposeHostField.match(i)
948 # # we ignore [[*..]] entries
949 # if m.startswith('*'):
951 # if m.startswith('-'):
954 # if not m.endswith(HostDomain):
956 # if not m.endswith('.'):
958 # for Line in DNSInfo:
959 # if isSSHFP.match(Line):
960 # Line = "%s\t%s" % (m, Line)
961 # F.write(Line + "\n")
963 # Oops, something unspeakable happened.
969 # Generate the BSMTP file
970 def GenBSMTP(accounts, File, HomePrefix):
973 F = open(File + ".tmp", "w")
975 # Write out the zone file entry for each user
977 if not 'dnsZoneEntry' in a: continue
978 if not a.is_active_user(): continue
981 for z in a["dnsZoneEntry"]:
982 Split = z.lower().split()
983 if Split[1].lower() == 'in':
984 for y in range(0, len(Split)):
987 Line = " ".join(Split) + "\n"
989 Host = Split[0] + DNSZone
990 if BSMTPCheck.match(Line) != None:
991 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
992 a['uid'], HomePrefix, a['uid'], Host))
995 F.write("; Errors\n")
998 # Oops, something unspeakable happened.
1004 def HostToIP(Host, mapped=True):
1008 if Host[1].has_key("ipHostNumber"):
1009 for addr in Host[1]["ipHostNumber"]:
1010 IPAdresses.append(addr)
1011 if not is_ipv6_addr(addr) and mapped == "True":
1012 IPAdresses.append("::ffff:"+addr)
1016 # Generate the ssh known hosts file
1017 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
1020 OldMask = os.umask(0022)
1021 F = open(File + ".tmp", "w", 0644)
1024 for x in host_attrs:
1025 if x[1].has_key("hostname") == 0 or \
1026 x[1].has_key("sshRSAHostKey") == 0:
1028 Host = GetAttr(x, "hostname")
1029 HostNames = [ Host ]
1030 if Host.endswith(HostDomain):
1031 HostNames.append(Host[:-(len(HostDomain) + 1)])
1033 # in the purpose field [[host|some other text]] (where some other text is optional)
1034 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1035 # file. But so that we don't have to add everything we link we can add an asterisk
1036 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
1037 # http linking it we also support [[-hostname]] entries.
1038 for i in x[1].get("purpose", []):
1039 m = PurposeHostField.match(i)
1042 # we ignore [[*..]] entries
1043 if m.startswith('*'):
1045 if m.startswith('-'):
1049 if m.endswith(HostDomain):
1050 HostNames.append(m[:-(len(HostDomain) + 1)])
1052 for I in x[1]["sshRSAHostKey"]:
1053 if mode and mode == 'authorized_keys':
1055 if 'sshdistAuthKeysHost' in x[1]:
1056 hosts += x[1]['sshdistAuthKeysHost']
1057 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1058 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1059 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1061 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1062 Line = Sanitize(Line) + "\n"
1064 # Oops, something unspeakable happened.
1070 # Generate the debianhosts file (list of all IP addresses)
1071 def GenHosts(host_attrs, File):
1074 OldMask = os.umask(0022)
1075 F = open(File + ".tmp", "w", 0644)
1080 for x in host_attrs:
1082 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1085 if not 'ipHostNumber' in x[1]:
1088 addrs = x[1]["ipHostNumber"]
1090 if addr not in seen:
1092 addr = Sanitize(addr) + "\n"
1095 # Oops, something unspeakable happened.
1101 def replaceTree(src, dst_basedir):
1102 bn = os.path.basename(src)
1103 dst = os.path.join(dst_basedir, bn)
1105 shutil.copytree(src, dst)
1107 def GenKeyrings(OutDir):
1109 if os.path.isdir(k):
1110 replaceTree(k, OutDir)
1112 shutil.copy(k, OutDir)
1115 def get_accounts(ldap_conn):
1116 # Fetch all the users
1117 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1118 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1119 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1120 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1121 "shadowExpire", "emailForward", "latitude", "longitude",\
1122 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1123 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1124 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1125 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1126 "mailContentInspectionAction", "webPassword", "rtcPassword",\
1129 if passwd_attrs is None:
1130 raise UDEmptyList, "No Users"
1131 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1132 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1136 def get_hosts(ldap_conn):
1137 # Fetch all the hosts
1138 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1139 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1140 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1142 if HostAttrs == None:
1143 raise UDEmptyList, "No Hosts"
1145 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1150 def make_ldap_conn():
1151 # Connect to the ldap server
1153 # for testing purposes it's sometimes useful to pass username/password
1154 # via the environment
1155 if 'UD_CREDENTIALS' in os.environ:
1156 Pass = os.environ['UD_CREDENTIALS'].split()
1158 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1159 Pass = F.readline().strip().split(" ")
1161 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1167 def setup_group_maps(l):
1168 # Fetch all the groups
1171 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1172 ["gid", "gidNumber", "subGroup"])
1174 # Generate the subgroup_map and group_id_map
1176 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1178 if x[1].has_key("gidNumber") == 0:
1180 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1181 if x[1].has_key("subGroup") != 0:
1182 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1186 SubGroupMap = subgroup_map
1187 GroupIDMap = group_id_map
1189 def generate_all(global_dir, ldap_conn):
1190 accounts = get_accounts(ldap_conn)
1191 host_attrs = get_hosts(ldap_conn)
1194 # Generate global things
1195 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1197 accounts = filter(lambda x: not IsRetired(x), accounts)
1199 CheckForward(accounts)
1201 GenMailDisable(accounts, global_dir + "mail-disable")
1202 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1203 GenDBM(accounts, global_dir + "mail-forward.db", 'emailForward')
1204 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1205 GenDBM(accounts, global_dir + "mail-contentinspectionaction.db", 'mailContentInspectionAction')
1206 GenPrivate(accounts, global_dir + "debian-private")
1207 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1208 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1209 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1210 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1211 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1212 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1213 GenWebPassword(accounts, global_dir + "web-passwords")
1214 GenRtcPassword(accounts, global_dir + "rtc-passwords")
1215 GenKeyrings(global_dir)
1218 GenForward(accounts, global_dir + "forward-alias")
1220 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1221 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1223 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1224 GenMarkers(accounts, global_dir + "markers")
1225 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1226 GenHosts(host_attrs, global_dir + "debianhosts")
1227 GenSSHGitolite(accounts, host_attrs, global_dir + "ssh-gitolite")
1229 GenDNS(accounts, global_dir + "dns-zone")
1230 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1232 setup_group_maps(ldap_conn)
1234 for host in host_attrs:
1235 if not "hostname" in host[1]:
1237 generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1239 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1240 current_host = host[1]['hostname'][0]
1241 OutDir = global_dir + current_host + '/'
1242 if not os.path.isdir(OutDir):
1245 # Get the group list and convert any named groups to numerics
1247 for groupname in AllowedGroupsPreload.strip().split(" "):
1248 GroupList[groupname] = True
1249 if 'allowedGroups' in host[1]:
1250 for groupname in host[1]['allowedGroups']:
1251 GroupList[groupname] = True
1252 for groupname in GroupList.keys():
1253 if groupname in GroupIDMap:
1254 GroupList[str(GroupIDMap[groupname])] = True
1257 if 'exportOptions' in host[1]:
1258 for extra in host[1]['exportOptions']:
1259 ExtraList[extra.upper()] = True
1262 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1264 DoLink(global_dir, OutDir, "debianhosts")
1265 DoLink(global_dir, OutDir, "ssh_known_hosts")
1266 DoLink(global_dir, OutDir, "disabled-accounts")
1269 if 'NOPASSWD' in ExtraList:
1270 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1272 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1274 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1275 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1277 # Now we know who we're allowing on the machine, export
1278 # the relevant ssh keys
1279 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1281 if not 'NOPASSWD' in ExtraList:
1282 GenShadow(accounts, OutDir + "shadow")
1284 # Link in global things
1285 if not 'NOMARKERS' in ExtraList:
1286 DoLink(global_dir, OutDir, "markers")
1287 DoLink(global_dir, OutDir, "mail-forward.cdb")
1288 DoLink(global_dir, OutDir, "mail-forward.db")
1289 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1290 DoLink(global_dir, OutDir, "mail-contentinspectionaction.db")
1291 DoLink(global_dir, OutDir, "mail-disable")
1292 DoLink(global_dir, OutDir, "mail-greylist")
1293 DoLink(global_dir, OutDir, "mail-callout")
1294 DoLink(global_dir, OutDir, "mail-rbl")
1295 DoLink(global_dir, OutDir, "mail-rhsbl")
1296 DoLink(global_dir, OutDir, "mail-whitelist")
1297 DoLink(global_dir, OutDir, "all-accounts.json")
1298 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1299 GenDBM(accounts, OutDir + "user-forward.db", 'emailForward')
1300 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1301 GenDBM(accounts, OutDir + "batv-tokens.db", 'bATVToken')
1302 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1303 GenDBM(accounts, OutDir + "default-mail-options.db", 'mailDefaultOptions')
1306 DoLink(global_dir, OutDir, "forward-alias")
1308 if 'DNS' in ExtraList:
1309 DoLink(global_dir, OutDir, "dns-zone")
1310 DoLink(global_dir, OutDir, "dns-sshfp")
1312 if 'AUTHKEYS' in ExtraList:
1313 DoLink(global_dir, OutDir, "authorized_keys")
1315 if 'BSMTP' in ExtraList:
1316 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1318 if 'PRIVATE' in ExtraList:
1319 DoLink(global_dir, OutDir, "debian-private")
1321 if 'GITOLITE' in ExtraList:
1322 DoLink(global_dir, OutDir, "ssh-gitolite")
1323 if 'exportOptions' in host[1]:
1324 for entry in host[1]['exportOptions']:
1325 v = entry.split('=',1)
1326 if v[0] != 'GITOLITE' or len(v) != 2: continue
1327 options = v[1].split(',')
1328 group = options.pop(0);
1329 gitolite_accounts = filter(lambda x: IsInGroup(x, [group], current_host), all_accounts)
1330 if not 'nohosts' in options:
1331 gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1336 if opt.startswith('sshcmd='):
1337 command = opt.split('=',1)[1]
1338 GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(group,), sshcommand=command, current_host=current_host)
1340 if 'WEB-PASSWORDS' in ExtraList:
1341 DoLink(global_dir, OutDir, "web-passwords")
1343 if 'RTC-PASSWORDS' in ExtraList:
1344 DoLink(global_dir, OutDir, "rtc-passwords")
1346 if 'KEYRING' in ExtraList:
1348 bn = os.path.basename(k)
1349 if os.path.isdir(k):
1350 src = os.path.join(global_dir, bn)
1351 replaceTree(src, OutDir)
1353 DoLink(global_dir, OutDir, bn)
1357 bn = os.path.basename(k)
1358 target = os.path.join(OutDir, bn)
1359 if os.path.isdir(target):
1362 posix.remove(target)
1365 DoLink(global_dir, OutDir, "last_update.trace")
1368 def getLastLDAPChangeTime(l):
1369 mods = l.search_s('cn=log',
1370 ldap.SCOPE_ONELEVEL,
1371 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1376 # Sort the list by reqEnd
1377 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1378 # Take the last element in the array
1379 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1383 def getLastKeyringChangeTime():
1386 mt = os.path.getmtime(k)
1392 def getLastBuildTime(gdir):
1393 cache_last_ldap_mod = 0
1394 cache_last_unix_mod = 0
1398 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1399 cache_last_mod=fd.read().split()
1401 cache_last_ldap_mod = cache_last_mod[0]
1402 cache_last_unix_mod = int(cache_last_mod[1])
1403 cache_last_run = int(cache_last_mod[2])
1404 except IndexError, ValueError:
1408 if e.errno == errno.ENOENT:
1413 return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1415 def mq_notify(options, message):
1416 options.section = 'dsa-udgenerate'
1417 options.config = '/etc/dsa/pubsub.conf'
1419 config = Config(options)
1421 'rabbit_userid': config.username,
1422 'rabbit_password': config.password,
1423 'rabbit_virtual_host': config.vhost,
1424 'rabbit_hosts': ['pubsub02.debian.org', 'pubsub01.debian.org'],
1430 'timestamp': int(time.time())
1434 conn = Connection(conf=conf)
1435 conn.topic_send(config.topic,
1437 exchange_name=config.exchange,
1444 parser = optparse.OptionParser()
1445 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1446 help="Output directory.")
1447 parser.add_option("-f", "--force", dest="force", action="store_true",
1448 help="Force generation, even if no update to LDAP has happened.")
1450 (options, args) = parser.parse_args()
1455 if options.generatedir is not None:
1456 generate_dir = os.environ['UD_GENERATEDIR']
1457 elif 'UD_GENERATEDIR' in os.environ:
1458 generate_dir = os.environ['UD_GENERATEDIR']
1460 generate_dir = GenerateDir
1463 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1464 lock = get_lock( lockf )
1466 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1469 l = make_ldap_conn()
1471 time_started = int(time.time())
1472 ldap_last_mod = getLastLDAPChangeTime(l)
1473 unix_last_mod = getLastKeyringChangeTime()
1474 cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1476 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)
1478 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1479 if need_update or options.force:
1480 msg = 'Update forced' if options.force else 'Update needed'
1481 generate_all(generate_dir, l)
1482 mq_notify(options, msg)
1483 last_run = int(time.time())
1484 fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1489 if __name__ == "__main__":
1490 if 'UD_PROFILE' in os.environ:
1493 cProfile.run('ud_generate()', "udg_prof")
1494 p = pstats.Stats('udg_prof')
1495 ##p.sort_stats('time').print_stats()
1496 p.sort_stats('cumulative').print_stats()
1502 # vim:set shiftwidth=3: