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)
82 """Return a pretty-printed XML string for the Element.
84 rough_string = ElementTree.tostring(elem, 'utf-8')
85 reparsed = minidom.parseString(rough_string)
86 return reparsed.toprettyxml(indent=" ")
88 def safe_makedirs(dir):
92 if e.errno == errno.EEXIST:
101 if e.errno == errno.ENOENT:
106 def get_lock(fn, wait=5*60):
109 ends = time.time() + wait
114 fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
118 if time.time() >= ends:
120 sl = min(sl*2, 10, ends - time.time())
126 return Str.translate(string.maketrans("\n\r\t", "$$$"))
128 def DoLink(From, To, File):
130 posix.remove(To + File)
133 posix.link(From + File, To + File)
135 def IsRetired(account):
137 Looks for accountStatus in the LDAP record and tries to
138 match it against one of the known retired statuses
141 status = account['accountStatus']
143 line = status.split()
146 if status == "inactive":
149 elif status == "memorial":
152 elif status == "retiring":
153 # We'll give them a few extra days over what we said
154 age = 6 * 31 * 24 * 60 * 60
156 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
164 # See if this user is in the group list
165 def IsInGroup(account, allowed, current_host):
166 # See if the primary group is in the list
167 if str(account['gidNumber']) in allowed: return True
169 # Check the host based ACL
170 if account.is_allowed_by_hostacl(current_host): return True
172 # See if there are supplementary groups
173 if not 'supplementaryGid' in account: return False
176 addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
182 def Die(File, F, Fdb):
188 os.remove(File + ".tmp")
192 os.remove(File + ".tdb.tmp")
196 def Done(File, F, Fdb):
199 os.rename(File + ".tmp", File)
202 os.rename(File + ".tdb.tmp", File + ".tdb")
204 # Generate the password list
205 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
208 F = open(File + ".tdb.tmp", "w")
213 # Do not let people try to buffer overflow some busted passwd parser.
214 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
216 userlist[a['uid']] = a['gidNumber']
217 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
223 HomePrefix, a['uid'],
225 line = Sanitize(line) + "\n"
226 F.write("0%u %s" % (i, line))
227 F.write(".%s %s" % (a['uid'], line))
228 F.write("=%d %s" % (a['uidNumber'], line))
231 # Oops, something unspeakable happened.
237 # Return the list of users so we know which keys to export
240 def GenAllUsers(accounts, file):
243 OldMask = os.umask(0022)
244 f = open(file + ".tmp", "w", 0644)
249 all.append( { 'uid': a['uid'],
250 'uidNumber': a['uidNumber'],
251 'active': a.pw_active() and a.shadow_active() } )
254 # Oops, something unspeakable happened.
260 # Generate the shadow list
261 def GenShadow(accounts, File):
264 OldMask = os.umask(0077)
265 F = open(File + ".tdb.tmp", "w", 0600)
270 # If the account is locked, mark it as such in shadow
271 # See Debian Bug #308229 for why we set it to 1 instead of 0
272 if not a.pw_active(): ShadowExpire = '1'
273 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
274 else: ShadowExpire = ''
277 values.append(a['uid'])
278 values.append(a.get_password())
279 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
280 if key in a: values.append(a[key])
281 else: values.append('')
282 values.append(ShadowExpire)
283 line = ':'.join(values)+':'
284 line = Sanitize(line) + "\n"
285 F.write("0%u %s" % (i, line))
286 F.write(".%s %s" % (a['uid'], line))
289 # Oops, something unspeakable happened.
295 # Generate the sudo passwd file
296 def GenShadowSudo(accounts, File, untrusted, current_host):
299 OldMask = os.umask(0077)
300 F = open(File + ".tmp", "w", 0600)
305 if 'sudoPassword' in a:
306 for entry in a['sudoPassword']:
307 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*-]+) ([^ ]+)$').match(entry)
310 uuid = Match.group(1)
311 status = Match.group(2)
312 hosts = Match.group(3)
313 cryptedpass = Match.group(4)
315 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
317 for_all = hosts == "*"
318 for_this_host = current_host in hosts.split(',')
319 if not (for_all or for_this_host):
321 # ignore * passwords for untrusted hosts, but copy host specific passwords
322 if for_all and untrusted:
325 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
330 Line = "%s:%s" % (a['uid'], Pass)
331 Line = Sanitize(Line) + "\n"
332 F.write("%s" % (Line))
334 # Oops, something unspeakable happened.
340 # Generate the sudo passwd file
341 def GenSSHGitolite(accounts, hosts, File, sshcommand=None, current_host=None):
343 if sshcommand is None:
344 sshcommand = GitoliteSSHCommand
346 OldMask = os.umask(0022)
347 F = open(File + ".tmp", "w", 0600)
350 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
352 if not 'sshRSAAuthKey' in a: continue
355 prefix = GitoliteSSHRestrictions
356 prefix = prefix.replace('@@COMMAND@@', sshcommand)
357 prefix = prefix.replace('@@USER@@', User)
358 for I in a["sshRSAAuthKey"]:
359 if I.startswith("allowed_hosts=") and ' ' in line:
360 if current_host is None:
362 machines, I = I.split('=', 1)[1].split(' ', 1)
363 if current_host not in machines.split(','):
364 continue # skip this key
366 if I.startswith('ssh-'):
367 line = "%s %s"%(prefix, I)
369 continue # do not allow keys with other restrictions that might conflict
370 line = Sanitize(line) + "\n"
373 for dn, attrs in hosts:
374 if not 'sshRSAHostKey' in attrs: continue
375 hostname = "host-" + attrs['hostname'][0]
376 prefix = GitoliteSSHRestrictions
377 prefix = prefix.replace('@@COMMAND@@', sshcommand)
378 prefix = prefix.replace('@@USER@@', hostname)
379 for I in attrs["sshRSAHostKey"]:
380 line = "%s %s"%(prefix, I)
381 line = Sanitize(line) + "\n"
384 # Oops, something unspeakable happened.
390 # Generate the shadow list
391 def GenSSHShadow(global_dir, accounts):
392 # Fetch all the users
396 if not 'sshRSAAuthKey' in a: continue
399 for I in a['sshRSAAuthKey']:
400 MultipleLine = "%s" % I
401 MultipleLine = Sanitize(MultipleLine)
402 contents.append(MultipleLine)
403 userkeys[a['uid']] = contents
406 # Generate the webPassword list
407 def GenWebPassword(accounts, File):
410 OldMask = os.umask(0077)
411 F = open(File, "w", 0600)
415 if not 'webPassword' in a: continue
416 if not a.pw_active(): continue
418 Pass = str(a['webPassword'])
419 Line = "%s:%s" % (a['uid'], Pass)
420 Line = Sanitize(Line) + "\n"
421 F.write("%s" % (Line))
427 # Generate the rtcPassword list
428 def GenRtcPassword(accounts, File):
431 OldMask = os.umask(0077)
432 F = open(File, "w", 0600)
436 if not 'rtcPassword' in a: continue
437 if not a.pw_active(): continue
439 Line = "%s@debian.org:%s:rtc.debian.org:AUTHORIZED" % (a['uid'], str(a['rtcPassword']))
440 Line = Sanitize(Line) + "\n"
441 F.write("%s" % (Line))
447 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
448 OldMask = os.umask(0077)
449 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
452 if f not in ssh_userkeys:
454 # If we're not exporting their primary group, don't export
457 if userlist[f] in grouprevmap.keys():
458 grname = grouprevmap[userlist[f]]
461 if int(userlist[f]) <= 100:
462 # In these cases, look it up in the normal way so we
463 # deal with cases where, for instance, users are in group
464 # users as their primary group.
465 grname = grp.getgrgid(userlist[f])[0]
470 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])
474 for line in ssh_userkeys[f]:
475 if line.startswith("allowed_hosts=") and ' ' in line:
476 machines, line = line.split('=', 1)[1].split(' ', 1)
477 if current_host not in machines.split(','):
478 continue # skip this key
481 continue # no keys for this host
482 contents = "\n".join(lines) + "\n"
484 to = tarfile.TarInfo(name=f)
485 # These will only be used where the username doesn't
486 # exist on the target system for some reason; hence,
487 # in those cases, the safest thing is for the file to
488 # be owned by root but group nobody. This deals with
489 # the bloody obscure case where the group fails to exist
490 # whilst the user does (in which case we want to avoid
491 # ending up with a file which is owned user:root to avoid
492 # a fairly obvious attack vector)
495 # Using the username / groupname fields avoids any need
496 # to give a shit^W^W^Wcare about the UIDoffset stuff.
500 to.mtime = int(time.time())
501 to.size = len(contents)
503 tf.addfile(to, StringIO(contents))
506 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
508 # add a list of groups to existing groups,
509 # including all subgroups thereof, recursively.
510 # basically this proceduces the transitive hull of the groups in
512 def addGroups(existingGroups, newGroups, uid, current_host):
513 for group in newGroups:
514 # if it's a <group>@host, split it and verify it's on the current host.
515 s = group.split('@', 1)
516 if len(s) == 2 and s[1] != current_host:
520 # let's see if we handled this group already
521 if group in existingGroups:
524 if not GroupIDMap.has_key(group):
525 print "Group", group, "does not exist but", uid, "is in it"
528 existingGroups.append(group)
530 if SubGroupMap.has_key(group):
531 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
533 # Generate the group list
534 def GenGroup(accounts, File, current_host):
538 F = open(File + ".tdb.tmp", "w")
540 # Generate the GroupMap
544 GroupHasPrimaryMembers = {}
546 # Sort them into a list of groups having a set of users
548 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
549 if not 'supplementaryGid' in a: continue
552 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
554 GroupMap[g].append(a['uid'])
556 # Output the group file.
558 for x in GroupMap.keys():
559 if not x in GroupIDMap:
562 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
565 grouprevmap[GroupIDMap[x]] = x
567 Line = "%s:x:%u:" % (x, GroupIDMap[x])
569 for I in GroupMap[x]:
570 Line = Line + ("%s%s" % (Comma, I))
572 Line = Sanitize(Line) + "\n"
573 F.write("0%u %s" % (J, Line))
574 F.write(".%s %s" % (x, Line))
575 F.write("=%u %s" % (GroupIDMap[x], Line))
578 # Oops, something unspeakable happened.
586 def CheckForward(accounts):
588 if not 'emailForward' in a: continue
592 # Do not allow people to try to buffer overflow busted parsers
593 if len(a['emailForward']) > 200: delete = True
594 # Check the forwarding address
595 elif EmailCheck.match(a['emailForward']) is None: delete = True
598 a.delete_mailforward()
600 # Generate the email forwarding list
601 def GenForward(accounts, File):
604 OldMask = os.umask(0022)
605 F = open(File + ".tmp", "w", 0644)
609 if not 'emailForward' in a: continue
610 Line = "%s: %s" % (a['uid'], a['emailForward'])
611 Line = Sanitize(Line) + "\n"
614 # Oops, something unspeakable happened.
620 def GenCDB(accounts, File, key):
623 OldMask = os.umask(0022)
624 # nothing else does the fsync stuff, so why do it here?
625 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
626 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
629 # Write out the email address for each user
631 if not key in a: continue
634 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
637 # Oops, something unspeakable happened.
641 if Fdb.close() != None:
642 raise "cdbmake gave an error"
644 def GenDBM(accounts, File, key):
646 OldMask = os.umask(0022)
647 fn = os.path.join(File).encode('ascii', 'ignore')
654 Fdb = dbm.open(fn, "c")
657 # Write out the email address for each user
659 if not key in a: continue
666 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
667 os.remove(File + ".db")
669 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
670 os.rename (File + ".db", File)
672 # Generate the anon XEarth marker file
673 def GenMarkers(accounts, File):
676 F = open(File + ".tmp", "w")
678 # Write out the position for each user
680 if not ('latitude' in a and 'longitude' in a): continue
682 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
683 Line = Sanitize(Line) + "\n"
688 # Oops, something unspeakable happened.
694 # Generate the debian-private subscription list
695 def GenPrivate(accounts, File):
698 F = open(File + ".tmp", "w")
700 # Write out the position for each user
702 if not a.is_active_user(): continue
703 if a.is_guest_account(): continue
704 if not 'privateSub' in a: continue
706 Line = "%s"%(a['privateSub'])
707 Line = Sanitize(Line) + "\n"
712 # Oops, something unspeakable happened.
718 # Generate a list of locked accounts
719 def GenDisabledAccounts(accounts, File):
722 F = open(File + ".tmp", "w")
723 disabled_accounts = []
725 # Fetch all the users
727 if a.pw_active(): continue
728 Line = "%s:%s" % (a['uid'], "Account is locked")
729 disabled_accounts.append(a)
730 F.write(Sanitize(Line) + "\n")
732 # Oops, something unspeakable happened.
737 return disabled_accounts
739 # Generate the list of local addresses that refuse all mail
740 def GenMailDisable(accounts, File):
743 F = open(File + ".tmp", "w")
746 if not 'mailDisableMessage' in a: continue
747 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
748 Line = Sanitize(Line) + "\n"
751 # Oops, something unspeakable happened.
757 # Generate a list of uids that should have boolean affects applied
758 def GenMailBool(accounts, File, key):
761 F = open(File + ".tmp", "w")
764 if not key in a: continue
765 if not a[key] == 'TRUE': continue
766 Line = "%s"%(a['uid'])
767 Line = Sanitize(Line) + "\n"
770 # Oops, something unspeakable happened.
776 # Generate a list of hosts for RBL or whitelist purposes.
777 def GenMailList(accounts, File, key):
780 F = open(File + ".tmp", "w")
782 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
783 else: validregex = re.compile('^[-\w.]+$')
786 if not key in a: continue
788 filtered = filter(lambda z: validregex.match(z), a[key])
789 if len(filtered) == 0: continue
790 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
791 line = a['uid'] + ': ' + ' : '.join(filtered)
792 line = Sanitize(line) + "\n"
795 # Oops, something unspeakable happened.
801 def isRoleAccount(account):
802 return 'debianRoleAccount' in account['objectClass']
804 # Generate the DNS Zone file
805 def GenDNS(accounts, File):
808 F = open(File + ".tmp", "w")
810 # Fetch all the users
813 # Write out the zone file entry for each user
815 if not 'dnsZoneEntry' in a: continue
816 if not a.is_active_user() and not isRoleAccount(a): continue
817 if a.is_guest_account(): continue
820 F.write("; %s\n"%(a.email_address()))
821 for z in a["dnsZoneEntry"]:
822 Split = z.lower().split()
823 if Split[1].lower() == 'in':
824 Line = " ".join(Split) + "\n"
827 Host = Split[0] + DNSZone
828 if BSMTPCheck.match(Line) != None:
829 F.write("; Has BSMTP\n")
831 # Write some identification information
832 if not RRs.has_key(Host):
833 if Split[2].lower() in ["a", "aaaa"]:
834 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
835 for y in a["keyFingerPrint"]:
836 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
840 Line = "; Err %s"%(str(Split))
845 F.write("; Errors:\n")
846 for line in str(e).split("\n"):
847 F.write("; %s\n"%(line))
850 # Oops, something unspeakable happened.
858 socket.inet_pton(socket.AF_INET6, i)
863 def ExtractDNSInfo(x):
867 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
870 if x[1].has_key("ipHostNumber"):
871 for I in x[1]["ipHostNumber"]:
873 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
875 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
879 if 'sshRSAHostKey' in x[1]:
880 for I in x[1]["sshRSAHostKey"]:
882 if Split[0] == 'ssh-rsa':
884 if Split[0] == 'ssh-dss':
886 if Split[0] == 'ssh-ed25519':
888 if Algorithm == None:
890 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
891 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
892 Fingerprint = hashlib.new('sha256', base64.decodestring(Split[1])).hexdigest()
893 DNSInfo.append("%sIN\tSSHFP\t%u 2 %s" % (TTLprefix, Algorithm, Fingerprint))
895 if 'architecture' in x[1]:
896 Arch = GetAttr(x, "architecture")
898 if x[1].has_key("machine"):
899 Mach = " " + GetAttr(x, "machine")
900 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
902 if x[1].has_key("mXRecord"):
903 for I in x[1]["mXRecord"]:
905 for e in MX_remap[I]:
906 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
908 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
912 # Generate the DNS records
913 def GenZoneRecords(host_attrs, File):
916 F = open(File + ".tmp", "w")
918 # Fetch all the hosts
920 if x[1].has_key("hostname") == 0:
923 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
926 DNSInfo = ExtractDNSInfo(x)
930 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
933 Line = "\t\t\t%s" % (Line)
937 # this would write sshfp lines for services on machines
938 # but we can't yet, since some are cnames and we'll make
939 # an invalid zonefile
941 # for i in x[1].get("purpose", []):
942 # m = PurposeHostField.match(i)
945 # # we ignore [[*..]] entries
946 # if m.startswith('*'):
948 # if m.startswith('-'):
951 # if not m.endswith(HostDomain):
953 # if not m.endswith('.'):
955 # for Line in DNSInfo:
956 # if isSSHFP.match(Line):
957 # Line = "%s\t%s" % (m, Line)
958 # F.write(Line + "\n")
960 # Oops, something unspeakable happened.
966 # Generate the BSMTP file
967 def GenBSMTP(accounts, File, HomePrefix):
970 F = open(File + ".tmp", "w")
972 # Write out the zone file entry for each user
974 if not 'dnsZoneEntry' in a: continue
975 if not a.is_active_user(): continue
978 for z in a["dnsZoneEntry"]:
979 Split = z.lower().split()
980 if Split[1].lower() == 'in':
981 for y in range(0, len(Split)):
984 Line = " ".join(Split) + "\n"
986 Host = Split[0] + DNSZone
987 if BSMTPCheck.match(Line) != None:
988 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
989 a['uid'], HomePrefix, a['uid'], Host))
992 F.write("; Errors\n")
995 # Oops, something unspeakable happened.
1001 def HostToIP(Host, mapped=True):
1005 if Host[1].has_key("ipHostNumber"):
1006 for addr in Host[1]["ipHostNumber"]:
1007 IPAdresses.append(addr)
1008 if not is_ipv6_addr(addr) and mapped == "True":
1009 IPAdresses.append("::ffff:"+addr)
1013 # Generate the ssh known hosts file
1014 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
1017 OldMask = os.umask(0022)
1018 F = open(File + ".tmp", "w", 0644)
1021 for x in host_attrs:
1022 if x[1].has_key("hostname") == 0 or \
1023 x[1].has_key("sshRSAHostKey") == 0:
1025 Host = GetAttr(x, "hostname")
1026 HostNames = [ Host ]
1027 if Host.endswith(HostDomain):
1028 HostNames.append(Host[:-(len(HostDomain) + 1)])
1030 # in the purpose field [[host|some other text]] (where some other text is optional)
1031 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1032 # file. But so that we don't have to add everything we link we can add an asterisk
1033 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
1034 # http linking it we also support [[-hostname]] entries.
1035 for i in x[1].get("purpose", []):
1036 m = PurposeHostField.match(i)
1039 # we ignore [[*..]] entries
1040 if m.startswith('*'):
1042 if m.startswith('-'):
1046 if m.endswith(HostDomain):
1047 HostNames.append(m[:-(len(HostDomain) + 1)])
1049 for I in x[1]["sshRSAHostKey"]:
1050 if mode and mode == 'authorized_keys':
1052 if 'sshdistAuthKeysHost' in x[1]:
1053 hosts += x[1]['sshdistAuthKeysHost']
1054 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1055 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1056 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1058 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1059 Line = Sanitize(Line) + "\n"
1061 # Oops, something unspeakable happened.
1067 # Generate the debianhosts file (list of all IP addresses)
1068 def GenHosts(host_attrs, File):
1071 OldMask = os.umask(0022)
1072 F = open(File + ".tmp", "w", 0644)
1077 for x in host_attrs:
1079 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1082 if not 'ipHostNumber' in x[1]:
1085 addrs = x[1]["ipHostNumber"]
1087 if addr not in seen:
1089 addr = Sanitize(addr) + "\n"
1092 # Oops, something unspeakable happened.
1098 def replaceTree(src, dst_basedir):
1099 bn = os.path.basename(src)
1100 dst = os.path.join(dst_basedir, bn)
1102 shutil.copytree(src, dst)
1104 def GenKeyrings(OutDir):
1106 if os.path.isdir(k):
1107 replaceTree(k, OutDir)
1109 shutil.copy(k, OutDir)
1112 def get_accounts(ldap_conn):
1113 # Fetch all the users
1114 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1115 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1116 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1117 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1118 "shadowExpire", "emailForward", "latitude", "longitude",\
1119 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1120 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1121 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1122 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1123 "mailContentInspectionAction", "webPassword", "rtcPassword",\
1126 if passwd_attrs is None:
1127 raise UDEmptyList, "No Users"
1128 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1129 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1133 def get_hosts(ldap_conn):
1134 # Fetch all the hosts
1135 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1136 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1137 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1139 if HostAttrs == None:
1140 raise UDEmptyList, "No Hosts"
1142 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1147 def make_ldap_conn():
1148 # Connect to the ldap server
1150 # for testing purposes it's sometimes useful to pass username/password
1151 # via the environment
1152 if 'UD_CREDENTIALS' in os.environ:
1153 Pass = os.environ['UD_CREDENTIALS'].split()
1155 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1156 Pass = F.readline().strip().split(" ")
1158 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1164 def setup_group_maps(l):
1165 # Fetch all the groups
1168 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1169 ["gid", "gidNumber", "subGroup"])
1171 # Generate the subgroup_map and group_id_map
1173 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1175 if x[1].has_key("gidNumber") == 0:
1177 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1178 if x[1].has_key("subGroup") != 0:
1179 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1183 SubGroupMap = subgroup_map
1184 GroupIDMap = group_id_map
1186 def generate_all(global_dir, ldap_conn):
1187 accounts = get_accounts(ldap_conn)
1188 host_attrs = get_hosts(ldap_conn)
1191 # Generate global things
1192 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1194 accounts = filter(lambda x: not IsRetired(x), accounts)
1196 CheckForward(accounts)
1198 GenMailDisable(accounts, global_dir + "mail-disable")
1199 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1200 GenDBM(accounts, global_dir + "mail-forward.db", 'emailForward')
1201 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1202 GenDBM(accounts, global_dir + "mail-contentinspectionaction.db", 'mailContentInspectionAction')
1203 GenPrivate(accounts, global_dir + "debian-private")
1204 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1205 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1206 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1207 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1208 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1209 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1210 GenWebPassword(accounts, global_dir + "web-passwords")
1211 GenRtcPassword(accounts, global_dir + "rtc-passwords")
1212 GenKeyrings(global_dir)
1215 GenForward(accounts, global_dir + "forward-alias")
1217 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1218 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1220 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1221 GenMarkers(accounts, global_dir + "markers")
1222 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1223 GenHosts(host_attrs, global_dir + "debianhosts")
1224 GenSSHGitolite(accounts, host_attrs, global_dir + "ssh-gitolite")
1226 GenDNS(accounts, global_dir + "dns-zone")
1227 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1229 setup_group_maps(ldap_conn)
1231 for host in host_attrs:
1232 if not "hostname" in host[1]:
1234 generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1236 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1237 current_host = host[1]['hostname'][0]
1238 OutDir = global_dir + current_host + '/'
1239 if not os.path.isdir(OutDir):
1242 # Get the group list and convert any named groups to numerics
1244 for groupname in AllowedGroupsPreload.strip().split(" "):
1245 GroupList[groupname] = True
1246 if 'allowedGroups' in host[1]:
1247 for groupname in host[1]['allowedGroups']:
1248 GroupList[groupname] = True
1249 for groupname in GroupList.keys():
1250 if groupname in GroupIDMap:
1251 GroupList[str(GroupIDMap[groupname])] = True
1254 if 'exportOptions' in host[1]:
1255 for extra in host[1]['exportOptions']:
1256 ExtraList[extra.upper()] = True
1259 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1261 DoLink(global_dir, OutDir, "debianhosts")
1262 DoLink(global_dir, OutDir, "ssh_known_hosts")
1263 DoLink(global_dir, OutDir, "disabled-accounts")
1266 if 'NOPASSWD' in ExtraList:
1267 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1269 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1271 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1272 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1274 # Now we know who we're allowing on the machine, export
1275 # the relevant ssh keys
1276 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1278 if not 'NOPASSWD' in ExtraList:
1279 GenShadow(accounts, OutDir + "shadow")
1281 # Link in global things
1282 if not 'NOMARKERS' in ExtraList:
1283 DoLink(global_dir, OutDir, "markers")
1284 DoLink(global_dir, OutDir, "mail-forward.cdb")
1285 DoLink(global_dir, OutDir, "mail-forward.db")
1286 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1287 DoLink(global_dir, OutDir, "mail-contentinspectionaction.db")
1288 DoLink(global_dir, OutDir, "mail-disable")
1289 DoLink(global_dir, OutDir, "mail-greylist")
1290 DoLink(global_dir, OutDir, "mail-callout")
1291 DoLink(global_dir, OutDir, "mail-rbl")
1292 DoLink(global_dir, OutDir, "mail-rhsbl")
1293 DoLink(global_dir, OutDir, "mail-whitelist")
1294 DoLink(global_dir, OutDir, "all-accounts.json")
1295 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1296 GenDBM(accounts, OutDir + "user-forward.db", 'emailForward')
1297 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1298 GenDBM(accounts, OutDir + "batv-tokens.db", 'bATVToken')
1299 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1300 GenDBM(accounts, OutDir + "default-mail-options.db", 'mailDefaultOptions')
1303 DoLink(global_dir, OutDir, "forward-alias")
1305 if 'DNS' in ExtraList:
1306 DoLink(global_dir, OutDir, "dns-zone")
1307 DoLink(global_dir, OutDir, "dns-sshfp")
1309 if 'AUTHKEYS' in ExtraList:
1310 DoLink(global_dir, OutDir, "authorized_keys")
1312 if 'BSMTP' in ExtraList:
1313 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1315 if 'PRIVATE' in ExtraList:
1316 DoLink(global_dir, OutDir, "debian-private")
1318 if 'GITOLITE' in ExtraList:
1319 DoLink(global_dir, OutDir, "ssh-gitolite")
1320 if 'exportOptions' in host[1]:
1321 for entry in host[1]['exportOptions']:
1322 v = entry.split('=',1)
1323 if v[0] != 'GITOLITE' or len(v) != 2: continue
1324 options = v[1].split(',')
1325 group = options.pop(0);
1326 gitolite_accounts = filter(lambda x: IsInGroup(x, [group], current_host), all_accounts)
1327 if not 'nohosts' in options:
1328 gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1333 if opt.startswith('sshcmd='):
1334 command = opt.split('=',1)[1]
1335 GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(group,), sshcommand=command, current_host=current_host)
1337 if 'WEB-PASSWORDS' in ExtraList:
1338 DoLink(global_dir, OutDir, "web-passwords")
1340 if 'RTC-PASSWORDS' in ExtraList:
1341 DoLink(global_dir, OutDir, "rtc-passwords")
1343 if 'KEYRING' in ExtraList:
1345 bn = os.path.basename(k)
1346 if os.path.isdir(k):
1347 src = os.path.join(global_dir, bn)
1348 replaceTree(src, OutDir)
1350 DoLink(global_dir, OutDir, bn)
1354 bn = os.path.basename(k)
1355 target = os.path.join(OutDir, bn)
1356 if os.path.isdir(target):
1359 posix.remove(target)
1362 DoLink(global_dir, OutDir, "last_update.trace")
1365 def getLastLDAPChangeTime(l):
1366 mods = l.search_s('cn=log',
1367 ldap.SCOPE_ONELEVEL,
1368 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1373 # Sort the list by reqEnd
1374 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1375 # Take the last element in the array
1376 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1380 def getLastKeyringChangeTime():
1383 mt = os.path.getmtime(k)
1389 def getLastBuildTime(gdir):
1390 cache_last_ldap_mod = 0
1391 cache_last_unix_mod = 0
1395 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1396 cache_last_mod=fd.read().split()
1398 cache_last_ldap_mod = cache_last_mod[0]
1399 cache_last_unix_mod = int(cache_last_mod[1])
1400 cache_last_run = int(cache_last_mod[2])
1401 except IndexError, ValueError:
1405 if e.errno == errno.ENOENT:
1410 return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1412 def mq_notify(options, message):
1413 options.section = 'dsa-udgenerate'
1414 options.config = '/etc/dsa/pubsub.conf'
1416 config = Config(options)
1418 'rabbit_userid': config.username,
1419 'rabbit_password': config.password,
1420 'rabbit_virtual_host': config.vhost,
1421 'rabbit_hosts': ['pubsub02.debian.org', 'pubsub01.debian.org'],
1427 'timestamp': int(time.time())
1431 conn = Connection(conf=conf)
1432 conn.topic_send(config.topic,
1434 exchange_name=config.exchange,
1441 parser = optparse.OptionParser()
1442 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1443 help="Output directory.")
1444 parser.add_option("-f", "--force", dest="force", action="store_true",
1445 help="Force generation, even if no update to LDAP has happened.")
1447 (options, args) = parser.parse_args()
1452 if options.generatedir is not None:
1453 generate_dir = os.environ['UD_GENERATEDIR']
1454 elif 'UD_GENERATEDIR' in os.environ:
1455 generate_dir = os.environ['UD_GENERATEDIR']
1457 generate_dir = GenerateDir
1460 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1461 lock = get_lock( lockf )
1463 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1466 l = make_ldap_conn()
1468 time_started = int(time.time())
1469 ldap_last_mod = getLastLDAPChangeTime(l)
1470 unix_last_mod = getLastKeyringChangeTime()
1471 cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1473 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)
1475 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1476 if need_update or options.force:
1477 msg = 'Update forced' if options.force else 'Update needed'
1478 generate_all(generate_dir, l)
1479 mq_notify(options, msg)
1480 last_run = int(time.time())
1481 fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1486 if __name__ == "__main__":
1487 if 'UD_PROFILE' in os.environ:
1490 cProfile.run('ud_generate()', "udg_prof")
1491 p = pstats.Stats('udg_prof')
1492 ##p.sort_stats('time').print_stats()
1493 p.sort_stats('cumulative').print_stats()
1499 # vim:set shiftwidth=3: