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 import string, re, time, ldap, optparse, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp
33 from userdir_ldap import *
34 from userdir_exceptions import *
37 from cStringIO import StringIO
39 from StringIO import StringIO
41 import simplejson as json
44 if not '__author__' in json.__dict__:
45 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
46 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
49 sys.stderr.write("You should probably not run ud-generate as root.\n")
62 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
64 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
65 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
66 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
67 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
68 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
69 isSSHFP = re.compile("^\s*IN\s+SSHFP")
70 DNSZone = ".debian.net"
71 Keyrings = ConfModule.sync_keyrings.split(":")
72 GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
75 def safe_makedirs(dir):
79 if e.errno == errno.EEXIST:
88 if e.errno == errno.ENOENT:
93 def get_lock(fn, wait=5*60, max_age=3600*6):
95 stat = os.stat(fn + '.lock')
96 if stat.st_mtime < time.time() - max_age:
97 sys.stderr.write("Removing stale lock %s"%(fn + '.lock'))
98 os.unlink(fn + '.lock')
99 except OSError, error:
100 if error.errno == errno.ENOENT:
105 lock = lockfile.FileLock(fn)
107 lock.acquire(timeout=wait)
108 except lockfile.LockTimeout:
115 return Str.translate(string.maketrans("\n\r\t", "$$$"))
117 def DoLink(From, To, File):
119 posix.remove(To + File)
122 posix.link(From + File, To + File)
124 def IsRetired(account):
126 Looks for accountStatus in the LDAP record and tries to
127 match it against one of the known retired statuses
130 status = account['accountStatus']
132 line = status.split()
135 if status == "inactive":
138 elif status == "memorial":
141 elif status == "retiring":
142 # We'll give them a few extra days over what we said
143 age = 6 * 31 * 24 * 60 * 60
145 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
153 #def IsGidDebian(account):
154 # return account['gidNumber'] == 800
156 # See if this user is in the group list
157 def IsInGroup(account, allowed):
158 # See if the primary group is in the list
159 if str(account['gidNumber']) in allowed: return True
161 # Check the host based ACL
162 if account.is_allowed_by_hostacl(CurrentHost): return True
164 # See if there are supplementary groups
165 if not 'supplementaryGid' in account: return False
168 addGroups(supgroups, account['supplementaryGid'], account['uid'])
170 if allowed.has_key(g):
174 def Die(File, F, Fdb):
180 os.remove(File + ".tmp")
184 os.remove(File + ".tdb.tmp")
188 def Done(File, F, Fdb):
191 os.rename(File + ".tmp", File)
194 os.rename(File + ".tdb.tmp", File + ".tdb")
196 # Generate the password list
197 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
200 F = open(File + ".tdb.tmp", "w")
205 # Do not let people try to buffer overflow some busted passwd parser.
206 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
208 userlist[a['uid']] = a['gidNumber']
209 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
215 HomePrefix, a['uid'],
217 line = Sanitize(line) + "\n"
218 F.write("0%u %s" % (i, line))
219 F.write(".%s %s" % (a['uid'], line))
220 F.write("=%d %s" % (a['uidNumber'], line))
223 # Oops, something unspeakable happened.
229 # Return the list of users so we know which keys to export
232 def GenAllUsers(accounts, file):
235 OldMask = os.umask(0022)
236 f = open(file + ".tmp", "w", 0644)
241 all.append( { 'uid': a['uid'],
242 'uidNumber': a['uidNumber'],
243 'active': a.pw_active() and a.shadow_active() } )
246 # Oops, something unspeakable happened.
252 # Generate the shadow list
253 def GenShadow(accounts, File):
256 OldMask = os.umask(0077)
257 F = open(File + ".tdb.tmp", "w", 0600)
262 # If the account is locked, mark it as such in shadow
263 # See Debian Bug #308229 for why we set it to 1 instead of 0
264 if not a.pw_active(): ShadowExpire = '1'
265 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
266 else: ShadowExpire = ''
269 values.append(a['uid'])
270 values.append(a.get_password())
271 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
272 if key in a: values.append(a[key])
273 else: values.append('')
274 values.append(ShadowExpire)
275 line = ':'.join(values)+':'
276 line = Sanitize(line) + "\n"
277 F.write("0%u %s" % (i, line))
278 F.write(".%s %s" % (a['uid'], line))
281 # Oops, something unspeakable happened.
287 # Generate the sudo passwd file
288 def GenShadowSudo(accounts, File, untrusted):
291 OldMask = os.umask(0077)
292 F = open(File + ".tmp", "w", 0600)
297 if 'sudoPassword' in a:
298 for entry in a['sudoPassword']:
299 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
302 uuid = Match.group(1)
303 status = Match.group(2)
304 hosts = Match.group(3)
305 cryptedpass = Match.group(4)
307 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
309 for_all = hosts == "*"
310 for_this_host = CurrentHost in hosts.split(',')
311 if not (for_all or for_this_host):
313 # ignore * passwords for untrusted hosts, but copy host specific passwords
314 if for_all and untrusted:
317 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
322 Line = "%s:%s" % (a['uid'], Pass)
323 Line = Sanitize(Line) + "\n"
324 F.write("%s" % (Line))
326 # Oops, something unspeakable happened.
332 # Generate the sudo passwd file
333 def GenSSHGitolite(accounts, File):
336 OldMask = os.umask(0022)
337 F = open(File + ".tmp", "w", 0600)
340 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
342 if not 'sshRSAAuthKey' in a: continue
345 prefix = GitoliteSSHRestrictions.replace('@@USER@@', User)
346 for I in a["sshRSAAuthKey"]:
347 if I.startswith('ssh-'):
348 line = "%s %s"%(prefix, I)
350 line = "%s,%s"%(prefix, I)
351 line = Sanitize(line) + "\n"
354 # Oops, something unspeakable happened.
360 # Generate the shadow list
361 def GenSSHShadow(global_dir, accounts):
362 # Fetch all the users
365 safe_rmtree(os.path.join(global_dir, 'userkeys'))
366 safe_makedirs(os.path.join(global_dir, 'userkeys'))
369 if not 'sshRSAAuthKey' in a: continue
373 OldMask = os.umask(0077)
374 File = os.path.join(global_dir, 'userkeys', a['uid'])
375 F = open(File + ".tmp", "w", 0600)
378 for I in a['sshRSAAuthKey']:
379 MultipleLine = "%s" % I
380 MultipleLine = Sanitize(MultipleLine) + "\n"
381 F.write(MultipleLine)
384 userfiles.append(os.path.basename(File))
386 # Oops, something unspeakable happened.
389 # As neither masterFileName nor masterFile are defined at any point
390 # this will raise a NameError.
391 Die(masterFileName, masterFile, None)
396 # Generate the webPassword list
397 def GenWebPassword(accounts, File):
400 OldMask = os.umask(0077)
401 F = open(File, "w", 0600)
405 if not 'webPassword' in a: continue
406 if not a.pw_active(): continue
408 Pass = str(a['webPassword'])
409 Line = "%s:%s" % (a['uid'], Pass)
410 Line = Sanitize(Line) + "\n"
411 F.write("%s" % (Line))
417 def GenSSHtarballs(global_dir, userlist, SSHFiles, grouprevmap, target):
418 OldMask = os.umask(0077)
419 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
421 for f in userlist.keys():
422 if f not in SSHFiles:
424 # If we're not exporting their primary group, don't export
427 if userlist[f] in grouprevmap.keys():
428 grname = grouprevmap[userlist[f]]
431 if int(userlist[f]) <= 100:
432 # In these cases, look it up in the normal way so we
433 # deal with cases where, for instance, users are in group
434 # users as their primary group.
435 grname = grp.getgrgid(userlist[f])[0]
440 print "User %s is supposed to have their key exported to host %s but their primary group (gid: %d) isn't in LDAP" % (f, CurrentHost, userlist[f])
443 to = tf.gettarinfo(os.path.join(global_dir, 'userkeys', f), f)
444 # These will only be used where the username doesn't
445 # exist on the target system for some reason; hence,
446 # in those cases, the safest thing is for the file to
447 # be owned by root but group nobody. This deals with
448 # the bloody obscure case where the group fails to exist
449 # whilst the user does (in which case we want to avoid
450 # ending up with a file which is owned user:root to avoid
451 # a fairly obvious attack vector)
454 # Using the username / groupname fields avoids any need
455 # to give a shit^W^W^Wcare about the UIDoffset stuff.
460 contents = file(os.path.join(global_dir, 'userkeys', f)).read()
462 for line in contents.splitlines():
463 if line.startswith("allowed_hosts=") and ' ' in line:
464 machines, line = line.split('=', 1)[1].split(' ', 1)
465 if CurrentHost not in machines.split(','):
466 continue # skip this key
469 continue # no keys for this host
470 contents = "\n".join(lines) + "\n"
471 to.size = len(contents)
472 tf.addfile(to, StringIO(contents))
475 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
477 # add a list of groups to existing groups,
478 # including all subgroups thereof, recursively.
479 # basically this proceduces the transitive hull of the groups in
481 def addGroups(existingGroups, newGroups, uid):
482 for group in newGroups:
483 # if it's a <group>@host, split it and verify it's on the current host.
484 s = group.split('@', 1)
485 if len(s) == 2 and s[1] != CurrentHost:
489 # let's see if we handled this group already
490 if group in existingGroups:
493 if not GroupIDMap.has_key(group):
494 print "Group", group, "does not exist but", uid, "is in it"
497 existingGroups.append(group)
499 if SubGroupMap.has_key(group):
500 addGroups(existingGroups, SubGroupMap[group], uid)
502 # Generate the group list
503 def GenGroup(accounts, File):
507 F = open(File + ".tdb.tmp", "w")
509 # Generate the GroupMap
511 for x in GroupIDMap.keys():
513 GroupHasPrimaryMembers = {}
515 # Sort them into a list of groups having a set of users
517 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
518 if not 'supplementaryGid' in a: continue
521 addGroups(supgroups, a['supplementaryGid'], a['uid'])
523 GroupMap[g].append(a['uid'])
525 # Output the group file.
527 for x in GroupMap.keys():
528 if GroupIDMap.has_key(x) == 0:
531 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
534 grouprevmap[GroupIDMap[x]] = x
536 Line = "%s:x:%u:" % (x, GroupIDMap[x])
538 for I in GroupMap[x]:
539 Line = Line + ("%s%s" % (Comma, I))
541 Line = Sanitize(Line) + "\n"
542 F.write("0%u %s" % (J, Line))
543 F.write(".%s %s" % (x, Line))
544 F.write("=%u %s" % (GroupIDMap[x], Line))
547 # Oops, something unspeakable happened.
555 def CheckForward(accounts):
557 if not 'emailForward' in a: continue
561 # Do not allow people to try to buffer overflow busted parsers
562 if len(a['emailForward']) > 200: delete = True
563 # Check the forwarding address
564 elif EmailCheck.match(a['emailForward']) is None: delete = True
567 a.delete_mailforward()
569 # Generate the email forwarding list
570 def GenForward(accounts, File):
573 OldMask = os.umask(0022)
574 F = open(File + ".tmp", "w", 0644)
578 if not 'emailForward' in a: continue
579 Line = "%s: %s" % (a['uid'], a['emailForward'])
580 Line = Sanitize(Line) + "\n"
583 # Oops, something unspeakable happened.
589 def GenCDB(accounts, File, key):
592 OldMask = os.umask(0022)
593 Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
596 # Write out the email address for each user
598 if not key in a: continue
601 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
604 # Oops, something unspeakable happened.
608 if Fdb.close() != None:
609 raise "cdbmake gave an error"
611 # Generate the anon XEarth marker file
612 def GenMarkers(accounts, File):
615 F = open(File + ".tmp", "w")
617 # Write out the position for each user
619 if not ('latitude' in a and 'longitude' in a): continue
621 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
622 Line = Sanitize(Line) + "\n"
627 # Oops, something unspeakable happened.
633 # Generate the debian-private subscription list
634 def GenPrivate(accounts, File):
637 F = open(File + ".tmp", "w")
639 # Write out the position for each user
641 if not a.is_active_user(): continue
642 if not 'privateSub' in a: continue
644 Line = "%s"%(a['privateSub'])
645 Line = Sanitize(Line) + "\n"
650 # Oops, something unspeakable happened.
656 # Generate a list of locked accounts
657 def GenDisabledAccounts(accounts, File):
660 F = open(File + ".tmp", "w")
661 disabled_accounts = []
663 # Fetch all the users
665 if a.pw_active(): continue
666 Line = "%s:%s" % (a['uid'], "Account is locked")
667 disabled_accounts.append(a)
668 F.write(Sanitize(Line) + "\n")
670 # Oops, something unspeakable happened.
675 return disabled_accounts
677 # Generate the list of local addresses that refuse all mail
678 def GenMailDisable(accounts, File):
681 F = open(File + ".tmp", "w")
684 if not 'mailDisableMessage' in a: continue
685 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
686 Line = Sanitize(Line) + "\n"
689 # Oops, something unspeakable happened.
695 # Generate a list of uids that should have boolean affects applied
696 def GenMailBool(accounts, File, key):
699 F = open(File + ".tmp", "w")
702 if not key in a: continue
703 if not a[key] == 'TRUE': continue
704 Line = "%s"%(a['uid'])
705 Line = Sanitize(Line) + "\n"
708 # Oops, something unspeakable happened.
714 # Generate a list of hosts for RBL or whitelist purposes.
715 def GenMailList(accounts, File, key):
718 F = open(File + ".tmp", "w")
720 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
721 else: validregex = re.compile('^[-\w.]+$')
724 if not key in a: continue
726 filtered = filter(lambda z: validregex.match(z), a[key])
727 if len(filtered) == 0: continue
728 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
729 line = a['uid'] + ': ' + ' : '.join(filtered)
730 line = Sanitize(line) + "\n"
733 # Oops, something unspeakable happened.
739 def isRoleAccount(account):
740 return 'debianRoleAccount' in account['objectClass']
742 # Generate the DNS Zone file
743 def GenDNS(accounts, File):
746 F = open(File + ".tmp", "w")
748 # Fetch all the users
751 # Write out the zone file entry for each user
753 if not 'dnsZoneEntry' in a: continue
754 if not a.is_active_user() and not isRoleAccount(a): continue
757 F.write("; %s\n"%(a.email_address()))
758 for z in a["dnsZoneEntry"]:
759 Split = z.lower().split()
760 if Split[1].lower() == 'in':
761 Line = " ".join(Split) + "\n"
764 Host = Split[0] + DNSZone
765 if BSMTPCheck.match(Line) != None:
766 F.write("; Has BSMTP\n")
768 # Write some identification information
769 if not RRs.has_key(Host):
770 if Split[2].lower() in ["a", "aaaa"]:
771 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
772 for y in a["keyFingerPrint"]:
773 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
777 Line = "; Err %s"%(str(Split))
782 F.write("; Errors:\n")
783 for line in str(e).split("\n"):
784 F.write("; %s\n"%(line))
787 # Oops, something unspeakable happened.
793 def ExtractDNSInfo(x):
797 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
800 if x[1].has_key("ipHostNumber"):
801 for I in x[1]["ipHostNumber"]:
802 if IsV6Addr.match(I) != None:
803 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
805 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
809 if 'sshRSAHostKey' in x[1]:
810 for I in x[1]["sshRSAHostKey"]:
812 if Split[0] == 'ssh-rsa':
814 if Split[0] == 'ssh-dss':
816 if Algorithm == None:
818 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
819 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
821 if 'architecture' in x[1]:
822 Arch = GetAttr(x, "architecture")
824 if x[1].has_key("machine"):
825 Mach = " " + GetAttr(x, "machine")
826 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
828 if x[1].has_key("mXRecord"):
829 for I in x[1]["mXRecord"]:
830 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
834 # Generate the DNS records
835 def GenZoneRecords(host_attrs, File):
838 F = open(File + ".tmp", "w")
840 # Fetch all the hosts
842 if x[1].has_key("hostname") == 0:
845 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
848 DNSInfo = ExtractDNSInfo(x)
852 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
855 Line = "\t\t\t%s" % (Line)
859 # this would write sshfp lines for services on machines
860 # but we can't yet, since some are cnames and we'll make
861 # an invalid zonefile
863 # for i in x[1].get("purpose", []):
864 # m = PurposeHostField.match(i)
867 # # we ignore [[*..]] entries
868 # if m.startswith('*'):
870 # if m.startswith('-'):
873 # if not m.endswith(HostDomain):
875 # if not m.endswith('.'):
877 # for Line in DNSInfo:
878 # if isSSHFP.match(Line):
879 # Line = "%s\t%s" % (m, Line)
880 # F.write(Line + "\n")
882 # Oops, something unspeakable happened.
888 # Generate the BSMTP file
889 def GenBSMTP(accounts, File, HomePrefix):
892 F = open(File + ".tmp", "w")
894 # Write out the zone file entry for each user
896 if not 'dnsZoneEntry' in a: continue
897 if not a.is_active_user(): continue
900 for z in a["dnsZoneEntry"]:
901 Split = z.lower().split()
902 if Split[1].lower() == 'in':
903 for y in range(0, len(Split)):
906 Line = " ".join(Split) + "\n"
908 Host = Split[0] + DNSZone
909 if BSMTPCheck.match(Line) != None:
910 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
911 a['uid'], HomePrefix, a['uid'], Host))
914 F.write("; Errors\n")
917 # Oops, something unspeakable happened.
923 def HostToIP(Host, mapped=True):
927 if Host[1].has_key("ipHostNumber"):
928 for addr in Host[1]["ipHostNumber"]:
929 IPAdresses.append(addr)
930 if IsV6Addr.match(addr) is None and mapped == "True":
931 IPAdresses.append("::ffff:"+addr)
935 # Generate the ssh known hosts file
936 def GenSSHKnown(host_attrs, File, mode=None):
939 OldMask = os.umask(0022)
940 F = open(File + ".tmp", "w", 0644)
944 if x[1].has_key("hostname") == 0 or \
945 x[1].has_key("sshRSAHostKey") == 0:
947 Host = GetAttr(x, "hostname")
949 if Host.endswith(HostDomain):
950 HostNames.append(Host[:-(len(HostDomain) + 1)])
952 # in the purpose field [[host|some other text]] (where some other text is optional)
953 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
954 # file. But so that we don't have to add everything we link we can add an asterisk
955 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
956 # http linking it we also support [[-hostname]] entries.
957 for i in x[1].get("purpose", []):
958 m = PurposeHostField.match(i)
961 # we ignore [[*..]] entries
962 if m.startswith('*'):
964 if m.startswith('-'):
968 if m.endswith(HostDomain):
969 HostNames.append(m[:-(len(HostDomain) + 1)])
971 for I in x[1]["sshRSAHostKey"]:
972 if mode and mode == 'authorized_keys':
974 if 'sshdistAuthKeysHost' in x[1]:
975 hosts += x[1]['sshdistAuthKeysHost']
976 Line = 'command="rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (Host, ",".join(hosts), I)
978 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
979 Line = Sanitize(Line) + "\n"
981 # Oops, something unspeakable happened.
987 # Generate the debianhosts file (list of all IP addresses)
988 def GenHosts(host_attrs, File):
991 OldMask = os.umask(0022)
992 F = open(File + ".tmp", "w", 0644)
999 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1002 if not 'ipHostNumber' in x[1]:
1005 addrs = x[1]["ipHostNumber"]
1007 if addr not in seen:
1009 addr = Sanitize(addr) + "\n"
1012 # Oops, something unspeakable happened.
1018 def replaceTree(src, dst_basedir):
1019 bn = os.path.basename(src)
1020 dst = os.path.join(dst_basedir, bn)
1022 shutil.copytree(src, dst)
1024 def GenKeyrings(OutDir):
1026 if os.path.isdir(k):
1027 replaceTree(k, OutDir)
1029 shutil.copy(k, OutDir)
1032 def get_accounts(ldap_conn):
1033 # Fetch all the users
1034 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1035 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1036 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1037 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1038 "shadowExpire", "emailForward", "latitude", "longitude",\
1039 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1040 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1041 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1042 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1043 "mailContentInspectionAction", "webPassword"])
1045 if passwd_attrs is None:
1046 raise UDEmptyList, "No Users"
1047 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1048 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1052 def get_hosts(ldap_conn):
1053 # Fetch all the hosts
1054 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1055 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1056 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1058 if HostAttrs == None:
1059 raise UDEmptyList, "No Hosts"
1061 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1066 def make_ldap_conn():
1067 # Connect to the ldap server
1069 # for testing purposes it's sometimes useful to pass username/password
1070 # via the environment
1071 if 'UD_CREDENTIALS' in os.environ:
1072 Pass = os.environ['UD_CREDENTIALS'].split()
1074 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1075 Pass = F.readline().strip().split(" ")
1077 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1081 def generate_all(global_dir, ldap_conn):
1082 accounts = get_accounts(ldap_conn)
1083 host_attrs = get_hosts(ldap_conn)
1086 # Generate global things
1087 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1089 accounts = filter(lambda x: not IsRetired(x), accounts)
1090 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1092 CheckForward(accounts)
1094 GenMailDisable(accounts, global_dir + "mail-disable")
1095 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1096 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1097 GenPrivate(accounts, global_dir + "debian-private")
1098 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1099 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1100 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1101 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1102 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1103 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1104 GenWebPassword(accounts, global_dir + "web-passwords")
1105 GenKeyrings(global_dir)
1108 GenForward(accounts, global_dir + "forward-alias")
1110 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1111 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1113 ssh_files = GenSSHShadow(global_dir, accounts)
1114 GenMarkers(accounts, global_dir + "markers")
1115 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1116 GenHosts(host_attrs, global_dir + "debianhosts")
1117 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1119 GenDNS(accounts, global_dir + "dns-zone")
1120 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1122 for host in host_attrs:
1123 if not "hostname" in host[1]:
1125 generate_host(host, global_dir, accounts, ssh_files)
1127 def generate_host(host, global_dir, accounts, ssh_files):
1130 CurrentHost = host[1]['hostname'][0]
1131 OutDir = global_dir + CurrentHost + '/'
1137 # Get the group list and convert any named groups to numerics
1139 for groupname in AllowedGroupsPreload.strip().split(" "):
1140 GroupList[groupname] = True
1141 if 'allowedGroups' in host[1]:
1142 for groupname in host[1]['allowedGroups']:
1143 GroupList[groupname] = True
1144 for groupname in GroupList.keys():
1145 if groupname in GroupIDMap:
1146 GroupList[str(GroupIDMap[groupname])] = True
1149 if 'exportOptions' in host[1]:
1150 for extra in host[1]['exportOptions']:
1151 ExtraList[extra.upper()] = True
1154 accounts = filter(lambda x: IsInGroup(x, GroupList), accounts)
1156 DoLink(global_dir, OutDir, "debianhosts")
1157 DoLink(global_dir, OutDir, "ssh_known_hosts")
1158 DoLink(global_dir, OutDir, "disabled-accounts")
1161 if 'NOPASSWD' in ExtraList:
1162 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1164 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1166 grouprevmap = GenGroup(accounts, OutDir + "group")
1167 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1169 # Now we know who we're allowing on the machine, export
1170 # the relevant ssh keys
1171 GenSSHtarballs(global_dir, userlist, ssh_files, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1173 if not 'NOPASSWD' in ExtraList:
1174 GenShadow(accounts, OutDir + "shadow")
1176 # Link in global things
1177 if not 'NOMARKERS' in ExtraList:
1178 DoLink(global_dir, OutDir, "markers")
1179 DoLink(global_dir, OutDir, "mail-forward.cdb")
1180 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1181 DoLink(global_dir, OutDir, "mail-disable")
1182 DoLink(global_dir, OutDir, "mail-greylist")
1183 DoLink(global_dir, OutDir, "mail-callout")
1184 DoLink(global_dir, OutDir, "mail-rbl")
1185 DoLink(global_dir, OutDir, "mail-rhsbl")
1186 DoLink(global_dir, OutDir, "mail-whitelist")
1187 DoLink(global_dir, OutDir, "all-accounts.json")
1188 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1189 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1190 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1193 DoLink(global_dir, OutDir, "forward-alias")
1195 if 'DNS' in ExtraList:
1196 DoLink(global_dir, OutDir, "dns-zone")
1197 DoLink(global_dir, OutDir, "dns-sshfp")
1199 if 'AUTHKEYS' in ExtraList:
1200 DoLink(global_dir, OutDir, "authorized_keys")
1202 if 'BSMTP' in ExtraList:
1203 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1205 if 'PRIVATE' in ExtraList:
1206 DoLink(global_dir, OutDir, "debian-private")
1208 if 'GITOLITE' in ExtraList:
1209 DoLink(global_dir, OutDir, "ssh-gitolite")
1211 if 'WEB-PASSWORDS' in ExtraList:
1212 DoLink(global_dir, OutDir, "web-passwords")
1214 if 'KEYRING' in ExtraList:
1216 bn = os.path.basename(k)
1217 if os.path.isdir(k):
1218 src = os.path.join(global_dir, bn)
1219 replaceTree(src, OutDir)
1221 DoLink(global_dir, OutDir, bn)
1225 bn = os.path.basename(k)
1226 target = os.path.join(OutDir, bn)
1227 if os.path.isdir(target):
1230 posix.remove(target)
1233 DoLink(global_dir, OutDir, "last_update.trace")
1236 def getLastLDAPChangeTime(l):
1237 mods = l.search_s('cn=log',
1238 ldap.SCOPE_ONELEVEL,
1239 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1244 # Sort the list by reqEnd
1245 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1246 # Take the last element in the array
1247 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1251 def getLastBuildTime():
1255 fd = open(os.path.join(GenerateDir, "last_update.trace"), "r")
1256 cache_last_mod=fd.read().split()
1258 cache_last_mod = cache_last_mod[0]
1263 if e.errno == errno.ENOENT:
1268 return cache_last_mod
1276 parser = optparse.OptionParser()
1277 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1278 help="Output directory.")
1279 parser.add_option("-f", "--force", dest="force", action="store_true",
1280 help="Force generation, even if not update to LDAP has happened.")
1282 (options, args) = parser.parse_args()
1288 l = make_ldap_conn()
1290 if options.generatedir is not None:
1291 GenerateDir = os.environ['UD_GENERATEDIR']
1292 elif 'UD_GENERATEDIR' in os.environ:
1293 GenerateDir = os.environ['UD_GENERATEDIR']
1295 ldap_last_mod = getLastLDAPChangeTime(l)
1296 cache_last_mod = getLastBuildTime()
1297 need_update = ldap_last_mod > cache_last_mod
1299 if not options.force and not need_update:
1300 fd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
1301 fd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1305 # Fetch all the groups
1307 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1308 ["gid", "gidNumber", "subGroup"])
1310 # Generate the SubGroupMap and GroupIDMap
1312 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1314 if x[1].has_key("gidNumber") == 0:
1316 GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1317 if x[1].has_key("subGroup") != 0:
1318 SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1322 lockf = os.path.join(GenerateDir, 'ud-generate.lock')
1323 lock = get_lock( lockf )
1325 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1328 tracefd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
1329 generate_all(GenerateDir, l)
1330 tracefd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1334 if lock is not None:
1337 if __name__ == "__main__":
1343 # vim:set shiftwidth=3: