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
36 from userdir_ldap import *
37 from userdir_exceptions import *
39 from xml.etree.ElementTree import Element, SubElement, Comment
40 from xml.etree import ElementTree
41 from xml.dom import minidom
43 from cStringIO import StringIO
45 from StringIO import StringIO
47 import simplejson as json
50 if '__author__' not in json.__dict__:
51 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
52 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
55 sys.stderr.write("You should probably not run ud-generate as root.\n")
67 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
70 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)(,\s*([^ <>@]+@[^ ,<>@]+))*$")
71 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
72 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
73 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
74 isSSHFP = re.compile("^\s*IN\s+SSHFP")
75 DNSZone = ".debian.net"
76 Keyrings = ConfModule.sync_keyrings.split(":")
77 GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
78 GitoliteSSHCommand = getattr(ConfModule, "gitolitesshcommand", None)
79 GitoliteExportHosts = re.compile(getattr(ConfModule, "gitoliteexporthosts", "."))
80 MX_remap = json.loads(ConfModule.MX_remap)
81 use_mq = getattr(ConfModule, "use_mq", True)
83 rtc_realm = getattr(ConfModule, "rtc_realm", None)
84 rtc_append = getattr(ConfModule, "rtc_append", None)
87 """Return a pretty-printed XML string for the Element.
89 rough_string = ElementTree.tostring(elem, 'utf-8')
90 reparsed = minidom.parseString(rough_string)
91 return reparsed.toprettyxml(indent=" ")
93 def safe_makedirs(dir):
97 if e.errno == errno.EEXIST:
102 def safe_rmtree(dir):
106 if e.errno == errno.ENOENT:
111 def get_lock(fn, wait=5*60):
114 ends = time.time() + wait
119 fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
123 if time.time() >= ends:
125 sl = min(sl*2, 10, ends - time.time())
131 return Str.translate(string.maketrans("\n\r\t", "$$$"))
133 def DoLink(From, To, File):
135 posix.remove(To + File)
138 posix.link(From + File, To + File)
140 def IsRetired(account):
142 Looks for accountStatus in the LDAP record and tries to
143 match it against one of the known retired statuses
146 status = account['accountStatus']
148 line = status.split()
151 if status == "inactive":
154 elif status == "memorial":
157 elif status == "retiring":
158 # We'll give them a few extra days over what we said
159 age = 6 * 31 * 24 * 60 * 60
161 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
169 # See if this user is in the group list
170 def IsInGroup(account, allowed, current_host):
171 # See if the primary group is in the list
172 if str(account['gidNumber']) in allowed: return True
174 # Check the host based ACL
175 if account.is_allowed_by_hostacl(current_host): return True
177 # See if there are supplementary groups
178 if 'supplementaryGid' not in account: return False
181 addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
187 def Die(File, F, Fdb):
193 os.remove(File + ".tmp")
197 os.remove(File + ".tdb.tmp")
201 def Done(File, F, Fdb):
204 os.rename(File + ".tmp", File)
207 os.rename(File + ".tdb.tmp", File + ".tdb")
209 # Generate the password list
210 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
213 F = open(File + ".tdb.tmp", "w")
218 if 'loginShell' not in a:
220 # Do not let people try to buffer overflow some busted passwd parser.
221 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
223 userlist[a['uid']] = a['gidNumber']
224 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
230 HomePrefix, a['uid'],
232 line = Sanitize(line) + "\n"
233 F.write("0%u %s" % (i, line))
234 F.write(".%s %s" % (a['uid'], line))
235 F.write("=%d %s" % (a['uidNumber'], line))
238 # Oops, something unspeakable happened.
244 # Return the list of users so we know which keys to export
247 def GenAllUsers(accounts, file):
250 OldMask = os.umask(0022)
251 f = open(file + ".tmp", "w", 0644)
256 all.append( { 'uid': a['uid'],
257 'uidNumber': a['uidNumber'],
258 'active': a.pw_active() and a.shadow_active() } )
261 # Oops, something unspeakable happened.
267 # Generate the shadow list
268 def GenShadow(accounts, File):
271 OldMask = os.umask(0077)
272 F = open(File + ".tdb.tmp", "w", 0600)
277 # If the account is locked, mark it as such in shadow
278 # See Debian Bug #308229 for why we set it to 1 instead of 0
279 if not a.pw_active(): ShadowExpire = '1'
280 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
281 else: ShadowExpire = ''
284 values.append(a['uid'])
285 values.append(a.get_password())
286 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
287 if key in a: values.append(a[key])
288 else: values.append('')
289 values.append(ShadowExpire)
290 line = ':'.join(values)+':'
291 line = Sanitize(line) + "\n"
292 F.write("0%u %s" % (i, line))
293 F.write(".%s %s" % (a['uid'], line))
296 # Oops, something unspeakable happened.
302 # Generate the sudo passwd file
303 def GenShadowSudo(accounts, File, untrusted, current_host):
306 OldMask = os.umask(0077)
307 F = open(File + ".tmp", "w", 0600)
312 if 'sudoPassword' in a:
313 for entry in a['sudoPassword']:
314 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*-]+) ([^ ]+)$').match(entry)
317 uuid = Match.group(1)
318 status = Match.group(2)
319 hosts = Match.group(3)
320 cryptedpass = Match.group(4)
322 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
324 for_all = hosts == "*"
325 for_this_host = current_host in hosts.split(',')
326 if not (for_all or for_this_host):
328 # ignore * passwords for untrusted hosts, but copy host specific passwords
329 if for_all and untrusted:
332 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
337 Line = "%s:%s" % (a['uid'], Pass)
338 Line = Sanitize(Line) + "\n"
339 F.write("%s" % (Line))
341 # Oops, something unspeakable happened.
347 # Generate the sudo passwd file
348 def GenSSHGitolite(accounts, hosts, File, sshcommand=None, current_host=None):
350 if sshcommand is None:
351 sshcommand = GitoliteSSHCommand
353 OldMask = os.umask(0022)
354 F = open(File + ".tmp", "w", 0600)
357 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
359 if 'sshRSAAuthKey' not in a: continue
362 prefix = GitoliteSSHRestrictions
363 prefix = prefix.replace('@@COMMAND@@', sshcommand)
364 prefix = prefix.replace('@@USER@@', User)
365 for I in a["sshRSAAuthKey"]:
366 if I.startswith("allowed_hosts=") and ' ' in line:
367 if current_host is None:
369 machines, I = I.split('=', 1)[1].split(' ', 1)
370 if current_host not in machines.split(','):
371 continue # skip this key
373 if I.startswith('ssh-'):
374 line = "%s %s"%(prefix, I)
376 continue # do not allow keys with other restrictions that might conflict
377 line = Sanitize(line) + "\n"
380 for dn, attrs in hosts:
381 if 'sshRSAHostKey' not in attrs: continue
382 hostname = "host-" + attrs['hostname'][0]
383 prefix = GitoliteSSHRestrictions
384 prefix = prefix.replace('@@COMMAND@@', sshcommand)
385 prefix = prefix.replace('@@USER@@', hostname)
386 for I in attrs["sshRSAHostKey"]:
387 line = "%s %s"%(prefix, I)
388 line = Sanitize(line) + "\n"
391 # Oops, something unspeakable happened.
397 # Generate the shadow list
398 def GenSSHShadow(global_dir, accounts):
399 # Fetch all the users
403 if 'sshRSAAuthKey' not in a: continue
406 for I in a['sshRSAAuthKey']:
407 MultipleLine = "%s" % I
408 MultipleLine = Sanitize(MultipleLine)
409 contents.append(MultipleLine)
410 userkeys[a['uid']] = contents
413 # Generate the webPassword list
414 def GenWebPassword(accounts, File):
417 OldMask = os.umask(0077)
418 F = open(File, "w", 0600)
422 if 'webPassword' not in a: continue
423 if not a.pw_active(): continue
425 Pass = str(a['webPassword'])
426 Line = "%s:%s" % (a['uid'], Pass)
427 Line = Sanitize(Line) + "\n"
428 F.write("%s" % (Line))
434 # Generate the rtcPassword list
435 def GenRtcPassword(accounts, File):
438 OldMask = os.umask(0077)
439 F = open(File, "w", 0600)
443 if a.is_guest_account(): continue
444 if 'rtcPassword' not in a: continue
445 if not a.pw_active(): continue
447 Line = "%s%s:%s:%s:AUTHORIZED" % (a['uid'], rtc_append, str(a['rtcPassword']), rtc_realm)
448 Line = Sanitize(Line) + "\n"
449 F.write("%s" % (Line))
455 # Generate the TOTP auth file
456 def GenTOTPSeed(accounts, File):
459 OldMask = os.umask(0077)
460 F = open(File, "w", 0600)
463 F.write("# Option User Prefix Seed\n")
465 if a.is_guest_account(): continue
466 if 'totpSeed' not in a: continue
467 if not a.pw_active(): continue
469 Line = "HOTP/T30/6 %s - %s" % (a['uid'], a['totpSeed'])
470 Line = Sanitize(Line) + "\n"
471 F.write("%s" % (Line))
477 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
478 OldMask = os.umask(0077)
479 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
482 if f not in ssh_userkeys:
484 # If we're not exporting their primary group, don't export
487 if userlist[f] in grouprevmap.keys():
488 grname = grouprevmap[userlist[f]]
491 if int(userlist[f]) <= 100:
492 # In these cases, look it up in the normal way so we
493 # deal with cases where, for instance, users are in group
494 # users as their primary group.
495 grname = grp.getgrgid(userlist[f])[0]
500 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])
504 for line in ssh_userkeys[f]:
505 if line.startswith("allowed_hosts=") and ' ' in line:
506 machines, line = line.split('=', 1)[1].split(' ', 1)
507 if current_host not in machines.split(','):
508 continue # skip this key
511 continue # no keys for this host
512 contents = "\n".join(lines) + "\n"
514 to = tarfile.TarInfo(name=f)
515 # These will only be used where the username doesn't
516 # exist on the target system for some reason; hence,
517 # in those cases, the safest thing is for the file to
518 # be owned by root but group nobody. This deals with
519 # the bloody obscure case where the group fails to exist
520 # whilst the user does (in which case we want to avoid
521 # ending up with a file which is owned user:root to avoid
522 # a fairly obvious attack vector)
525 # Using the username / groupname fields avoids any need
526 # to give a shit^W^W^Wcare about the UIDoffset stuff.
530 to.mtime = int(time.time())
531 to.size = len(contents)
533 tf.addfile(to, StringIO(contents))
536 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
538 # add a list of groups to existing groups,
539 # including all subgroups thereof, recursively.
540 # basically this proceduces the transitive hull of the groups in
542 def addGroups(existingGroups, newGroups, uid, current_host):
543 for group in newGroups:
544 # if it's a <group>@host, split it and verify it's on the current host.
545 s = group.split('@', 1)
546 if len(s) == 2 and s[1] != current_host:
550 # let's see if we handled this group already
551 if group in existingGroups:
554 if not GroupIDMap.has_key(group):
555 print "Group", group, "does not exist but", uid, "is in it"
558 existingGroups.append(group)
560 if SubGroupMap.has_key(group):
561 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
563 # Generate the group list
564 def GenGroup(accounts, File, current_host):
568 F = open(File + ".tdb.tmp", "w")
570 # Generate the GroupMap
574 GroupHasPrimaryMembers = {}
576 # Sort them into a list of groups having a set of users
578 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
579 if 'supplementaryGid' not in a: continue
582 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
584 GroupMap[g].append(a['uid'])
586 # Output the group file.
588 for x in GroupMap.keys():
589 if x not in GroupIDMap:
592 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
595 grouprevmap[GroupIDMap[x]] = x
597 Line = "%s:x:%u:" % (x, GroupIDMap[x])
599 for I in GroupMap[x]:
600 Line = Line + ("%s%s" % (Comma, I))
602 Line = Sanitize(Line) + "\n"
603 F.write("0%u %s" % (J, Line))
604 F.write(".%s %s" % (x, Line))
605 F.write("=%u %s" % (GroupIDMap[x], Line))
608 # Oops, something unspeakable happened.
616 def CheckForward(accounts):
618 if 'emailForward' not in a: continue
622 # Do not allow people to try to buffer overflow busted parsers
623 if len(a['emailForward']) > 200: delete = True
624 # Check the forwarding address
625 elif EmailCheck.match(a['emailForward']) is None: delete = True
628 a.delete_mailforward()
630 # Generate the email forwarding list
631 def GenForward(accounts, File):
634 OldMask = os.umask(0022)
635 F = open(File + ".tmp", "w", 0644)
639 if 'emailForward' not in a: continue
640 Line = "%s: %s" % (a['uid'], a['emailForward'])
641 Line = Sanitize(Line) + "\n"
644 # Oops, something unspeakable happened.
650 def GenCDB(accounts, File, key):
651 prefix = ["/usr/bin/eatmydata"] if os.path.exists('/usr/bin/eatmydata') else []
652 # nothing else does the fsync stuff, so why do it here?
653 Fdb = subprocess.Popen(prefix + ["cdbmake", File, "%s.tmp" % File],
654 preexec_fn=lambda: os.umask(0022),
655 stdin=subprocess.PIPE)
657 # Write out the email address for each user
659 if key not in a: continue
662 Fdb.stdin.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
664 Fdb.stdin.write("\n")
668 raise Exception("cdbmake gave an error")
670 def GenDBM(accounts, File, key):
672 OldMask = os.umask(0022)
673 fn = os.path.join(File).encode('ascii', 'ignore')
680 Fdb = dbm.open(fn, "c")
683 # Write out the email address for each user
685 if key not in a: continue
692 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
693 os.remove(File + ".db")
695 # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
696 os.rename (File + ".db", File)
698 # Generate the anon XEarth marker file
699 def GenMarkers(accounts, File):
702 F = open(File + ".tmp", "w")
704 # Write out the position for each user
706 if not ('latitude' in a and 'longitude' in a): continue
708 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
709 Line = Sanitize(Line) + "\n"
714 # Oops, something unspeakable happened.
720 # Generate the debian-private subscription list
721 def GenPrivate(accounts, File):
724 F = open(File + ".tmp", "w")
726 # Write out the position for each user
728 if not a.is_active_user(): continue
729 if a.is_guest_account(): continue
730 if 'privateSub' not in a: continue
732 Line = "%s"%(a['privateSub'])
733 Line = Sanitize(Line) + "\n"
738 # Oops, something unspeakable happened.
744 # Generate a list of locked accounts
745 def GenDisabledAccounts(accounts, File):
748 F = open(File + ".tmp", "w")
749 disabled_accounts = []
751 # Fetch all the users
753 if a.pw_active(): continue
754 Line = "%s:%s" % (a['uid'], "Account is locked")
755 disabled_accounts.append(a)
756 F.write(Sanitize(Line) + "\n")
758 # Oops, something unspeakable happened.
763 return disabled_accounts
765 # Generate the list of local addresses that refuse all mail
766 def GenMailDisable(accounts, File):
769 F = open(File + ".tmp", "w")
772 if 'mailDisableMessage' not in a: continue
773 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
774 Line = Sanitize(Line) + "\n"
777 # Oops, something unspeakable happened.
783 # Generate a list of uids that should have boolean affects applied
784 def GenMailBool(accounts, File, key):
787 F = open(File + ".tmp", "w")
790 if key not in a: continue
791 if not a[key] == 'TRUE': continue
792 Line = "%s"%(a['uid'])
793 Line = Sanitize(Line) + "\n"
796 # Oops, something unspeakable happened.
802 # Generate a list of hosts for RBL or whitelist purposes.
803 def GenMailList(accounts, File, key):
806 F = open(File + ".tmp", "w")
808 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
809 else: validregex = re.compile('^[-\w.]+$')
812 if key not in a: continue
814 filtered = filter(lambda z: validregex.match(z), a[key])
815 if len(filtered) == 0: continue
816 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
817 line = a['uid'] + ': ' + ' : '.join(filtered)
818 line = Sanitize(line) + "\n"
821 # Oops, something unspeakable happened.
827 def isRoleAccount(account):
828 return 'debianRoleAccount' in account['objectClass']
830 # Generate the DNS Zone file
831 def GenDNS(accounts, File):
834 F = open(File + ".tmp", "w")
836 # Fetch all the users
839 # Write out the zone file entry for each user
841 if 'dnsZoneEntry' not in a: continue
842 if not a.is_active_user() and not isRoleAccount(a): continue
843 if a.is_guest_account(): continue
846 F.write("; %s\n"%(a.email_address()))
847 for z in a["dnsZoneEntry"]:
848 Split = z.lower().split()
849 if Split[1].lower() == 'in':
850 Line = " ".join(Split) + "\n"
853 Host = Split[0] + DNSZone
854 if BSMTPCheck.match(Line) is not None:
855 F.write("; Has BSMTP\n")
857 # Write some identification information
858 if not RRs.has_key(Host):
859 if Split[2].lower() in ["a", "aaaa"]:
860 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
861 for y in a["keyFingerPrint"]:
862 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
866 Line = "; Err %s"%(str(Split))
871 F.write("; Errors:\n")
872 for line in str(e).split("\n"):
873 F.write("; %s\n"%(line))
876 # Oops, something unspeakable happened.
884 socket.inet_pton(socket.AF_INET6, i)
889 def ExtractDNSInfo(x):
890 hostname = GetAttr(x, "hostname")
894 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
897 if x[1].has_key("ipHostNumber"):
898 for I in x[1]["ipHostNumber"]:
900 DNSInfo.append("%s.\t%sIN\tAAAA\t%s" % (hostname, TTLprefix, I))
902 DNSInfo.append("%s.\t%sIN\tA\t%s" % (hostname, TTLprefix, I))
906 ssh_hostnames = [ hostname ]
907 if x[1].has_key("sshfpHostname"):
908 ssh_hostnames += [ h for h in x[1]["sshfpHostname"] ]
910 if 'sshRSAHostKey' in x[1]:
911 for I in x[1]["sshRSAHostKey"]:
913 key_prefix = Split[0]
914 key = base64.decodestring(Split[1])
917 # https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml
918 if key_prefix == 'ssh-rsa':
920 if key_prefix == 'ssh-dss':
922 if key_prefix == 'ssh-ed25519':
924 if Algorithm is None:
926 # and more from the registry
927 sshfp_digest_codepoints = [ (1, 'sha1'), (2, 'sha256') ]
929 fingerprints = [ ( digest_codepoint, hashlib.new(algorithm, key).hexdigest() ) for digest_codepoint, algorithm in sshfp_digest_codepoints ]
930 for h in ssh_hostnames:
931 for digest_codepoint, fingerprint in fingerprints:
932 DNSInfo.append("%s.\t%sIN\tSSHFP\t%u %d %s" % (h, TTLprefix, Algorithm, digest_codepoint, fingerprint))
934 if 'architecture' in x[1]:
935 Arch = GetAttr(x, "architecture")
937 if x[1].has_key("machine"):
938 Mach = " " + GetAttr(x, "machine")
939 DNSInfo.append("%s.\t%sIN\tHINFO\t\"%s%s\" \"%s\"" % (hostname, TTLprefix, Arch, Mach, "Debian"))
941 if x[1].has_key("mXRecord"):
942 for I in x[1]["mXRecord"]:
944 for e in MX_remap[I]:
945 DNSInfo.append("%s.\t%sIN\tMX\t%s" % (hostname, TTLprefix, e))
947 DNSInfo.append("%s.\t%sIN\tMX\t%s" % (hostname, TTLprefix, I))
951 # Generate the DNS records
952 def GenZoneRecords(host_attrs, File):
955 F = open(File + ".tmp", "w")
957 # Fetch all the hosts
959 if x[1].has_key("hostname") == 0:
962 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
965 for Line in ExtractDNSInfo(x):
968 # Oops, something unspeakable happened.
974 # Generate the BSMTP file
975 def GenBSMTP(accounts, File, HomePrefix):
978 F = open(File + ".tmp", "w")
980 # Write out the zone file entry for each user
982 if 'dnsZoneEntry' not in a: continue
983 if not a.is_active_user(): continue
986 for z in a["dnsZoneEntry"]:
987 Split = z.lower().split()
988 if Split[1].lower() == 'in':
989 for y in range(0, len(Split)):
992 Line = " ".join(Split) + "\n"
994 Host = Split[0] + DNSZone
995 if BSMTPCheck.match(Line) is not None:
996 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
997 a['uid'], HomePrefix, a['uid'], Host))
1000 F.write("; Errors\n")
1003 # Oops, something unspeakable happened.
1009 def HostToIP(Host, mapped=True):
1013 if Host[1].has_key("ipHostNumber"):
1014 for addr in Host[1]["ipHostNumber"]:
1015 IPAdresses.append(addr)
1016 if not is_ipv6_addr(addr) and mapped == "True":
1017 IPAdresses.append("::ffff:"+addr)
1021 # Generate the ssh known hosts file
1022 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
1025 OldMask = os.umask(0022)
1026 F = open(File + ".tmp", "w", 0644)
1029 for x in host_attrs:
1030 if x[1].has_key("hostname") == 0 or \
1031 x[1].has_key("sshRSAHostKey") == 0:
1033 Host = GetAttr(x, "hostname")
1034 HostNames = [ Host ]
1035 if Host.endswith(HostDomain):
1036 HostNames.append(Host[:-(len(HostDomain) + 1)])
1038 # in the purpose field [[host|some other text]] (where some other text is optional)
1039 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1040 # file. But so that we don't have to add everything we link we can add an asterisk
1041 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
1042 # http linking it we also support [[-hostname]] entries.
1043 for i in x[1].get("purpose", []):
1044 m = PurposeHostField.match(i)
1047 # we ignore [[*..]] entries
1048 if m.startswith('*'):
1050 if m.startswith('-'):
1054 if m.endswith(HostDomain):
1055 HostNames.append(m[:-(len(HostDomain) + 1)])
1057 for I in x[1]["sshRSAHostKey"]:
1058 if mode and mode == 'authorized_keys':
1060 if 'sshdistAuthKeysHost' in x[1]:
1061 hosts += x[1]['sshdistAuthKeysHost']
1062 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1063 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1064 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1066 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1067 Line = Sanitize(Line) + "\n"
1069 # Oops, something unspeakable happened.
1075 # Generate the debianhosts file (list of all IP addresses)
1076 def GenHosts(host_attrs, File):
1079 OldMask = os.umask(0022)
1080 F = open(File + ".tmp", "w", 0644)
1085 for x in host_attrs:
1087 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1090 if 'ipHostNumber' not in x[1]:
1093 addrs = x[1]["ipHostNumber"]
1095 if addr not in seen:
1097 addr = Sanitize(addr) + "\n"
1100 # Oops, something unspeakable happened.
1106 def replaceTree(src, dst_basedir):
1107 bn = os.path.basename(src)
1108 dst = os.path.join(dst_basedir, bn)
1110 shutil.copytree(src, dst)
1112 def GenKeyrings(OutDir):
1114 if os.path.isdir(k):
1115 replaceTree(k, OutDir)
1117 shutil.copy(k, OutDir)
1120 def get_accounts(ldap_conn):
1121 # Fetch all the users
1122 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1123 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1124 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1125 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1126 "shadowExpire", "emailForward", "latitude", "longitude",\
1127 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1128 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1129 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1130 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1131 "mailContentInspectionAction", "webPassword", "rtcPassword",\
1132 "bATVToken", "totpSeed", "mailDefaultOptions"])
1134 if passwd_attrs is None:
1135 raise UDEmptyList, "No Users"
1136 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1137 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1141 def get_hosts(ldap_conn):
1142 # Fetch all the hosts
1143 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1144 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1145 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture",
1148 if HostAttrs is None:
1149 raise UDEmptyList, "No Hosts"
1151 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1156 def make_ldap_conn():
1157 # Connect to the ldap server
1159 # for testing purposes it's sometimes useful to pass username/password
1160 # via the environment
1161 if 'UD_CREDENTIALS' in os.environ:
1162 Pass = os.environ['UD_CREDENTIALS'].split()
1164 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1165 Pass = F.readline().strip().split(" ")
1167 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1173 def setup_group_maps(l):
1174 # Fetch all the groups
1177 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1178 ["gid", "gidNumber", "subGroup"])
1180 # Generate the subgroup_map and group_id_map
1182 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1184 if x[1].has_key("gidNumber") == 0:
1186 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1187 if x[1].has_key("subGroup") != 0:
1188 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1192 SubGroupMap = subgroup_map
1193 GroupIDMap = group_id_map
1195 def generate_all(global_dir, ldap_conn):
1196 accounts = get_accounts(ldap_conn)
1197 host_attrs = get_hosts(ldap_conn)
1200 # Generate global things
1201 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1203 accounts = filter(lambda x: not IsRetired(x), accounts)
1205 CheckForward(accounts)
1207 GenMailDisable(accounts, global_dir + "mail-disable")
1208 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1209 GenDBM(accounts, global_dir + "mail-forward.db", 'emailForward')
1210 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1211 GenDBM(accounts, global_dir + "mail-contentinspectionaction.db", 'mailContentInspectionAction')
1212 GenCDB(accounts, global_dir + "default-mail-options.cdb", 'mailDefaultOptions')
1213 GenDBM(accounts, global_dir + "default-mail-options.db", 'mailDefaultOptions')
1214 GenPrivate(accounts, global_dir + "debian-private")
1215 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1216 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1217 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1218 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1219 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1220 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1221 GenWebPassword(accounts, global_dir + "web-passwords")
1222 GenRtcPassword(accounts, global_dir + "rtc-passwords")
1223 GenTOTPSeed(accounts, global_dir + "users.oath")
1224 GenKeyrings(global_dir)
1227 GenForward(accounts, global_dir + "forward-alias")
1229 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1230 accounts = filter(lambda a: a not in accounts_disabled, accounts)
1232 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1233 GenMarkers(accounts, global_dir + "markers")
1234 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1235 GenHosts(host_attrs, global_dir + "debianhosts")
1237 GenDNS(accounts, global_dir + "dns-zone")
1238 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1240 setup_group_maps(ldap_conn)
1242 for host in host_attrs:
1243 if "hostname" not in host[1]:
1245 generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1247 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1248 current_host = host[1]['hostname'][0]
1249 OutDir = global_dir + current_host + '/'
1250 if not os.path.isdir(OutDir):
1253 # Get the group list and convert any named groups to numerics
1255 for groupname in AllowedGroupsPreload.strip().split(" "):
1256 GroupList[groupname] = True
1257 if 'allowedGroups' in host[1]:
1258 for groupname in host[1]['allowedGroups']:
1259 GroupList[groupname] = True
1260 for groupname in GroupList.keys():
1261 if groupname in GroupIDMap:
1262 GroupList[str(GroupIDMap[groupname])] = True
1265 if 'exportOptions' in host[1]:
1266 for extra in host[1]['exportOptions']:
1267 ExtraList[extra.upper()] = True
1270 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1272 DoLink(global_dir, OutDir, "debianhosts")
1273 DoLink(global_dir, OutDir, "ssh_known_hosts")
1274 DoLink(global_dir, OutDir, "disabled-accounts")
1277 if 'NOPASSWD' in ExtraList:
1278 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1280 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1282 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1283 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1285 # Now we know who we're allowing on the machine, export
1286 # the relevant ssh keys
1287 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1289 if 'NOPASSWD' not in ExtraList:
1290 GenShadow(accounts, OutDir + "shadow")
1292 # Link in global things
1293 if 'NOMARKERS' not in ExtraList:
1294 DoLink(global_dir, OutDir, "markers")
1295 DoLink(global_dir, OutDir, "mail-forward.cdb")
1296 DoLink(global_dir, OutDir, "mail-forward.db")
1297 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1298 DoLink(global_dir, OutDir, "mail-contentinspectionaction.db")
1299 DoLink(global_dir, OutDir, "mail-disable")
1300 DoLink(global_dir, OutDir, "mail-greylist")
1301 DoLink(global_dir, OutDir, "mail-callout")
1302 DoLink(global_dir, OutDir, "mail-rbl")
1303 DoLink(global_dir, OutDir, "mail-rhsbl")
1304 DoLink(global_dir, OutDir, "mail-whitelist")
1305 DoLink(global_dir, OutDir, "all-accounts.json")
1306 DoLink(global_dir, OutDir, "default-mail-options.cdb")
1307 DoLink(global_dir, OutDir, "default-mail-options.db")
1308 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1309 GenDBM(accounts, OutDir + "user-forward.db", 'emailForward')
1310 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1311 GenDBM(accounts, OutDir + "batv-tokens.db", 'bATVToken')
1314 DoLink(global_dir, OutDir, "forward-alias")
1316 if 'DNS' in ExtraList:
1317 DoLink(global_dir, OutDir, "dns-zone")
1318 DoLink(global_dir, OutDir, "dns-sshfp")
1320 if 'AUTHKEYS' in ExtraList:
1321 DoLink(global_dir, OutDir, "authorized_keys")
1323 if 'BSMTP' in ExtraList:
1324 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1326 if 'PRIVATE' in ExtraList:
1327 DoLink(global_dir, OutDir, "debian-private")
1329 if 'GITOLITE' in ExtraList:
1330 GenSSHGitolite(all_accounts, all_hosts, OutDir + "ssh-gitolite", current_host=current_host)
1331 if 'exportOptions' in host[1]:
1332 for entry in host[1]['exportOptions']:
1333 v = entry.split('=',1)
1334 if v[0] != 'GITOLITE' or len(v) != 2: continue
1335 options = v[1].split(',')
1336 group = options.pop(0)
1337 gitolite_accounts = filter(lambda x: IsInGroup(x, [group], current_host), all_accounts)
1338 if 'nohosts' not in options:
1339 gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1344 if opt.startswith('sshcmd='):
1345 command = opt.split('=',1)[1]
1346 GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(group,), sshcommand=command, current_host=current_host)
1348 if 'WEB-PASSWORDS' in ExtraList:
1349 DoLink(global_dir, OutDir, "web-passwords")
1351 if 'RTC-PASSWORDS' in ExtraList:
1352 DoLink(global_dir, OutDir, "rtc-passwords")
1354 if 'TOTP' in ExtraList:
1355 DoLink(global_dir, OutDir, "users.oath")
1357 if 'KEYRING' in ExtraList:
1359 bn = os.path.basename(k)
1360 if os.path.isdir(k):
1361 src = os.path.join(global_dir, bn)
1362 replaceTree(src, OutDir)
1364 DoLink(global_dir, OutDir, bn)
1368 bn = os.path.basename(k)
1369 target = os.path.join(OutDir, bn)
1370 if os.path.isdir(target):
1373 posix.remove(target)
1376 DoLink(global_dir, OutDir, "last_update.trace")
1379 def getLastLDAPChangeTime(l):
1380 mods = l.search_s('cn=log',
1381 ldap.SCOPE_ONELEVEL,
1382 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1387 # Sort the list by reqEnd
1388 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1389 # Take the last element in the array
1390 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1394 def getLastKeyringChangeTime():
1397 mt = os.path.getmtime(k)
1403 def getLastBuildTime(gdir):
1404 cache_last_ldap_mod = 0
1405 cache_last_unix_mod = 0
1409 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1410 cache_last_mod=fd.read().split()
1412 cache_last_ldap_mod = cache_last_mod[0]
1413 cache_last_unix_mod = int(cache_last_mod[1])
1414 cache_last_run = int(cache_last_mod[2])
1415 except IndexError, ValueError:
1419 if e.errno == errno.ENOENT:
1424 return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1426 def mq_notify(options, message):
1427 options.section = 'dsa-udgenerate'
1428 options.config = '/etc/dsa/pubsub.conf'
1430 config = Config(options)
1432 'rabbit_userid': config.username,
1433 'rabbit_password': config.password,
1434 'rabbit_virtual_host': config.vhost,
1435 'rabbit_hosts': ['pubsub02.debian.org', 'pubsub01.debian.org'],
1441 'timestamp': int(time.time())
1445 conn = Connection(conf=conf)
1446 conn.topic_send(config.topic,
1448 exchange_name=config.exchange,
1455 parser = optparse.OptionParser()
1456 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1457 help="Output directory.")
1458 parser.add_option("-f", "--force", dest="force", action="store_true",
1459 help="Force generation, even if no update to LDAP has happened.")
1461 (options, args) = parser.parse_args()
1466 if options.generatedir is not None:
1467 generate_dir = os.environ['UD_GENERATEDIR']
1468 elif 'UD_GENERATEDIR' in os.environ:
1469 generate_dir = os.environ['UD_GENERATEDIR']
1471 generate_dir = GenerateDir
1474 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1475 lock = get_lock( lockf )
1477 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1480 l = make_ldap_conn()
1482 time_started = int(time.time())
1483 ldap_last_mod = getLastLDAPChangeTime(l)
1484 unix_last_mod = getLastKeyringChangeTime()
1485 cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1487 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)
1489 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1490 if need_update or options.force:
1491 msg = 'Update forced' if options.force else 'Update needed'
1492 generate_all(generate_dir, l)
1494 mq_notify(options, msg)
1495 last_run = int(time.time())
1496 fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1501 if __name__ == "__main__":
1502 if 'UD_PROFILE' in os.environ:
1505 cProfile.run('ud_generate()', "udg_prof")
1506 p = pstats.Stats('udg_prof')
1507 ##p.sort_stats('time').print_stats()
1508 p.sort_stats('cumulative').print_stats()
1514 # vim:set shiftwidth=3: