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")
61 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
63 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
64 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
65 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
66 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
67 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
68 isSSHFP = re.compile("^\s*IN\s+SSHFP")
69 DNSZone = ".debian.net"
70 Keyrings = ConfModule.sync_keyrings.split(":")
71 GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
74 def safe_makedirs(dir):
78 if e.errno == errno.EEXIST:
87 if e.errno == errno.ENOENT:
92 def get_lock(fn, wait=5*60, max_age=3600*6):
94 stat = os.stat(fn + '.lock')
95 if stat.st_mtime < time.time() - max_age:
96 sys.stderr.write("Removing stale lock %s"%(fn + '.lock'))
97 os.unlink(fn + '.lock')
98 except OSError, error:
99 if error.errno == errno.ENOENT:
104 lock = lockfile.FileLock(fn)
106 lock.acquire(timeout=wait)
107 except lockfile.LockTimeout:
114 return Str.translate(string.maketrans("\n\r\t", "$$$"))
116 def DoLink(From, To, File):
118 posix.remove(To + File)
121 posix.link(From + File, To + File)
123 def IsRetired(account):
125 Looks for accountStatus in the LDAP record and tries to
126 match it against one of the known retired statuses
129 status = account['accountStatus']
131 line = status.split()
134 if status == "inactive":
137 elif status == "memorial":
140 elif status == "retiring":
141 # We'll give them a few extra days over what we said
142 age = 6 * 31 * 24 * 60 * 60
144 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
152 #def IsGidDebian(account):
153 # return account['gidNumber'] == 800
155 # See if this user is in the group list
156 def IsInGroup(account, allowed, current_host):
157 # See if the primary group is in the list
158 if str(account['gidNumber']) in allowed: return True
160 # Check the host based ACL
161 if account.is_allowed_by_hostacl(current_host): return True
163 # See if there are supplementary groups
164 if not 'supplementaryGid' in account: return False
167 addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
169 if allowed.has_key(g):
173 def Die(File, F, Fdb):
179 os.remove(File + ".tmp")
183 os.remove(File + ".tdb.tmp")
187 def Done(File, F, Fdb):
190 os.rename(File + ".tmp", File)
193 os.rename(File + ".tdb.tmp", File + ".tdb")
195 # Generate the password list
196 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
199 F = open(File + ".tdb.tmp", "w")
204 # Do not let people try to buffer overflow some busted passwd parser.
205 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
207 userlist[a['uid']] = a['gidNumber']
208 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
214 HomePrefix, a['uid'],
216 line = Sanitize(line) + "\n"
217 F.write("0%u %s" % (i, line))
218 F.write(".%s %s" % (a['uid'], line))
219 F.write("=%d %s" % (a['uidNumber'], line))
222 # Oops, something unspeakable happened.
228 # Return the list of users so we know which keys to export
231 def GenAllUsers(accounts, file):
234 OldMask = os.umask(0022)
235 f = open(file + ".tmp", "w", 0644)
240 all.append( { 'uid': a['uid'],
241 'uidNumber': a['uidNumber'],
242 'active': a.pw_active() and a.shadow_active() } )
245 # Oops, something unspeakable happened.
251 # Generate the shadow list
252 def GenShadow(accounts, File):
255 OldMask = os.umask(0077)
256 F = open(File + ".tdb.tmp", "w", 0600)
261 # If the account is locked, mark it as such in shadow
262 # See Debian Bug #308229 for why we set it to 1 instead of 0
263 if not a.pw_active(): ShadowExpire = '1'
264 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
265 else: ShadowExpire = ''
268 values.append(a['uid'])
269 values.append(a.get_password())
270 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
271 if key in a: values.append(a[key])
272 else: values.append('')
273 values.append(ShadowExpire)
274 line = ':'.join(values)+':'
275 line = Sanitize(line) + "\n"
276 F.write("0%u %s" % (i, line))
277 F.write(".%s %s" % (a['uid'], line))
280 # Oops, something unspeakable happened.
286 # Generate the sudo passwd file
287 def GenShadowSudo(accounts, File, untrusted, current_host):
290 OldMask = os.umask(0077)
291 F = open(File + ".tmp", "w", 0600)
296 if 'sudoPassword' in a:
297 for entry in a['sudoPassword']:
298 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
301 uuid = Match.group(1)
302 status = Match.group(2)
303 hosts = Match.group(3)
304 cryptedpass = Match.group(4)
306 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
308 for_all = hosts == "*"
309 for_this_host = current_host in hosts.split(',')
310 if not (for_all or for_this_host):
312 # ignore * passwords for untrusted hosts, but copy host specific passwords
313 if for_all and untrusted:
316 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
321 Line = "%s:%s" % (a['uid'], Pass)
322 Line = Sanitize(Line) + "\n"
323 F.write("%s" % (Line))
325 # Oops, something unspeakable happened.
331 # Generate the sudo passwd file
332 def GenSSHGitolite(accounts, File):
335 OldMask = os.umask(0022)
336 F = open(File + ".tmp", "w", 0600)
339 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
341 if not 'sshRSAAuthKey' in a: continue
344 prefix = GitoliteSSHRestrictions.replace('@@USER@@', User)
345 for I in a["sshRSAAuthKey"]:
346 if I.startswith('ssh-'):
347 line = "%s %s"%(prefix, I)
349 line = "%s,%s"%(prefix, I)
350 line = Sanitize(line) + "\n"
353 # Oops, something unspeakable happened.
359 # Generate the shadow list
360 def GenSSHShadow(global_dir, accounts):
361 # Fetch all the users
365 if not 'sshRSAAuthKey' in a: continue
368 for I in a['sshRSAAuthKey']:
369 MultipleLine = "%s" % I
370 MultipleLine = Sanitize(MultipleLine)
371 contents.append(MultipleLine)
372 userkeys[a['uid']] = contents
375 # Generate the webPassword list
376 def GenWebPassword(accounts, File):
379 OldMask = os.umask(0077)
380 F = open(File, "w", 0600)
384 if not 'webPassword' in a: continue
385 if not a.pw_active(): continue
387 Pass = str(a['webPassword'])
388 Line = "%s:%s" % (a['uid'], Pass)
389 Line = Sanitize(Line) + "\n"
390 F.write("%s" % (Line))
396 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
397 OldMask = os.umask(0077)
398 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
401 if f not in ssh_userkeys:
403 # If we're not exporting their primary group, don't export
406 if userlist[f] in grouprevmap.keys():
407 grname = grouprevmap[userlist[f]]
410 if int(userlist[f]) <= 100:
411 # In these cases, look it up in the normal way so we
412 # deal with cases where, for instance, users are in group
413 # users as their primary group.
414 grname = grp.getgrgid(userlist[f])[0]
419 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])
423 for line in ssh_userkeys[f]:
424 if line.startswith("allowed_hosts=") and ' ' in line:
425 machines, line = line.split('=', 1)[1].split(' ', 1)
426 if current_host not in machines.split(','):
427 continue # skip this key
430 continue # no keys for this host
431 contents = "\n".join(lines) + "\n"
433 to = tarfile.TarInfo(name=f)
434 # These will only be used where the username doesn't
435 # exist on the target system for some reason; hence,
436 # in those cases, the safest thing is for the file to
437 # be owned by root but group nobody. This deals with
438 # the bloody obscure case where the group fails to exist
439 # whilst the user does (in which case we want to avoid
440 # ending up with a file which is owned user:root to avoid
441 # a fairly obvious attack vector)
444 # Using the username / groupname fields avoids any need
445 # to give a shit^W^W^Wcare about the UIDoffset stuff.
449 to.mtime = int(time.time())
450 to.size = len(contents)
452 tf.addfile(to, StringIO(contents))
455 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
457 # add a list of groups to existing groups,
458 # including all subgroups thereof, recursively.
459 # basically this proceduces the transitive hull of the groups in
461 def addGroups(existingGroups, newGroups, uid, current_host):
462 for group in newGroups:
463 # if it's a <group>@host, split it and verify it's on the current host.
464 s = group.split('@', 1)
465 if len(s) == 2 and s[1] != current_host:
469 # let's see if we handled this group already
470 if group in existingGroups:
473 if not GroupIDMap.has_key(group):
474 print "Group", group, "does not exist but", uid, "is in it"
477 existingGroups.append(group)
479 if SubGroupMap.has_key(group):
480 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
482 # Generate the group list
483 def GenGroup(accounts, File, current_host):
487 F = open(File + ".tdb.tmp", "w")
489 # Generate the GroupMap
493 GroupHasPrimaryMembers = {}
495 # Sort them into a list of groups having a set of users
497 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
498 if not 'supplementaryGid' in a: continue
501 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
503 GroupMap[g].append(a['uid'])
505 # Output the group file.
507 for x in GroupMap.keys():
508 if not x in GroupIDMap:
511 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
514 grouprevmap[GroupIDMap[x]] = x
516 Line = "%s:x:%u:" % (x, GroupIDMap[x])
518 for I in GroupMap[x]:
519 Line = Line + ("%s%s" % (Comma, I))
521 Line = Sanitize(Line) + "\n"
522 F.write("0%u %s" % (J, Line))
523 F.write(".%s %s" % (x, Line))
524 F.write("=%u %s" % (GroupIDMap[x], Line))
527 # Oops, something unspeakable happened.
535 def CheckForward(accounts):
537 if not 'emailForward' in a: continue
541 # Do not allow people to try to buffer overflow busted parsers
542 if len(a['emailForward']) > 200: delete = True
543 # Check the forwarding address
544 elif EmailCheck.match(a['emailForward']) is None: delete = True
547 a.delete_mailforward()
549 # Generate the email forwarding list
550 def GenForward(accounts, File):
553 OldMask = os.umask(0022)
554 F = open(File + ".tmp", "w", 0644)
558 if not 'emailForward' in a: continue
559 Line = "%s: %s" % (a['uid'], a['emailForward'])
560 Line = Sanitize(Line) + "\n"
563 # Oops, something unspeakable happened.
569 def GenCDB(accounts, File, key):
572 OldMask = os.umask(0022)
573 # nothing else does the fsync stuff, so why do it here?
574 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
575 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
578 # Write out the email address for each user
580 if not key in a: continue
583 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
586 # Oops, something unspeakable happened.
590 if Fdb.close() != None:
591 raise "cdbmake gave an error"
593 # Generate the anon XEarth marker file
594 def GenMarkers(accounts, File):
597 F = open(File + ".tmp", "w")
599 # Write out the position for each user
601 if not ('latitude' in a and 'longitude' in a): continue
603 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
604 Line = Sanitize(Line) + "\n"
609 # Oops, something unspeakable happened.
615 # Generate the debian-private subscription list
616 def GenPrivate(accounts, File):
619 F = open(File + ".tmp", "w")
621 # Write out the position for each user
623 if not a.is_active_user(): continue
624 if not 'privateSub' in a: continue
626 Line = "%s"%(a['privateSub'])
627 Line = Sanitize(Line) + "\n"
632 # Oops, something unspeakable happened.
638 # Generate a list of locked accounts
639 def GenDisabledAccounts(accounts, File):
642 F = open(File + ".tmp", "w")
643 disabled_accounts = []
645 # Fetch all the users
647 if a.pw_active(): continue
648 Line = "%s:%s" % (a['uid'], "Account is locked")
649 disabled_accounts.append(a)
650 F.write(Sanitize(Line) + "\n")
652 # Oops, something unspeakable happened.
657 return disabled_accounts
659 # Generate the list of local addresses that refuse all mail
660 def GenMailDisable(accounts, File):
663 F = open(File + ".tmp", "w")
666 if not 'mailDisableMessage' in a: continue
667 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
668 Line = Sanitize(Line) + "\n"
671 # Oops, something unspeakable happened.
677 # Generate a list of uids that should have boolean affects applied
678 def GenMailBool(accounts, File, key):
681 F = open(File + ".tmp", "w")
684 if not key in a: continue
685 if not a[key] == 'TRUE': continue
686 Line = "%s"%(a['uid'])
687 Line = Sanitize(Line) + "\n"
690 # Oops, something unspeakable happened.
696 # Generate a list of hosts for RBL or whitelist purposes.
697 def GenMailList(accounts, File, key):
700 F = open(File + ".tmp", "w")
702 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
703 else: validregex = re.compile('^[-\w.]+$')
706 if not key in a: continue
708 filtered = filter(lambda z: validregex.match(z), a[key])
709 if len(filtered) == 0: continue
710 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
711 line = a['uid'] + ': ' + ' : '.join(filtered)
712 line = Sanitize(line) + "\n"
715 # Oops, something unspeakable happened.
721 def isRoleAccount(account):
722 return 'debianRoleAccount' in account['objectClass']
724 # Generate the DNS Zone file
725 def GenDNS(accounts, File):
728 F = open(File + ".tmp", "w")
730 # Fetch all the users
733 # Write out the zone file entry for each user
735 if not 'dnsZoneEntry' in a: continue
736 if not a.is_active_user() and not isRoleAccount(a): continue
739 F.write("; %s\n"%(a.email_address()))
740 for z in a["dnsZoneEntry"]:
741 Split = z.lower().split()
742 if Split[1].lower() == 'in':
743 Line = " ".join(Split) + "\n"
746 Host = Split[0] + DNSZone
747 if BSMTPCheck.match(Line) != None:
748 F.write("; Has BSMTP\n")
750 # Write some identification information
751 if not RRs.has_key(Host):
752 if Split[2].lower() in ["a", "aaaa"]:
753 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
754 for y in a["keyFingerPrint"]:
755 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
759 Line = "; Err %s"%(str(Split))
764 F.write("; Errors:\n")
765 for line in str(e).split("\n"):
766 F.write("; %s\n"%(line))
769 # Oops, something unspeakable happened.
775 def ExtractDNSInfo(x):
779 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
782 if x[1].has_key("ipHostNumber"):
783 for I in x[1]["ipHostNumber"]:
784 if IsV6Addr.match(I) != None:
785 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
787 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
791 if 'sshRSAHostKey' in x[1]:
792 for I in x[1]["sshRSAHostKey"]:
794 if Split[0] == 'ssh-rsa':
796 if Split[0] == 'ssh-dss':
798 if Algorithm == None:
800 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
801 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
803 if 'architecture' in x[1]:
804 Arch = GetAttr(x, "architecture")
806 if x[1].has_key("machine"):
807 Mach = " " + GetAttr(x, "machine")
808 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
810 if x[1].has_key("mXRecord"):
811 for I in x[1]["mXRecord"]:
812 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
816 # Generate the DNS records
817 def GenZoneRecords(host_attrs, File):
820 F = open(File + ".tmp", "w")
822 # Fetch all the hosts
824 if x[1].has_key("hostname") == 0:
827 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
830 DNSInfo = ExtractDNSInfo(x)
834 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
837 Line = "\t\t\t%s" % (Line)
841 # this would write sshfp lines for services on machines
842 # but we can't yet, since some are cnames and we'll make
843 # an invalid zonefile
845 # for i in x[1].get("purpose", []):
846 # m = PurposeHostField.match(i)
849 # # we ignore [[*..]] entries
850 # if m.startswith('*'):
852 # if m.startswith('-'):
855 # if not m.endswith(HostDomain):
857 # if not m.endswith('.'):
859 # for Line in DNSInfo:
860 # if isSSHFP.match(Line):
861 # Line = "%s\t%s" % (m, Line)
862 # F.write(Line + "\n")
864 # Oops, something unspeakable happened.
870 # Generate the BSMTP file
871 def GenBSMTP(accounts, File, HomePrefix):
874 F = open(File + ".tmp", "w")
876 # Write out the zone file entry for each user
878 if not 'dnsZoneEntry' in a: continue
879 if not a.is_active_user(): continue
882 for z in a["dnsZoneEntry"]:
883 Split = z.lower().split()
884 if Split[1].lower() == 'in':
885 for y in range(0, len(Split)):
888 Line = " ".join(Split) + "\n"
890 Host = Split[0] + DNSZone
891 if BSMTPCheck.match(Line) != None:
892 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
893 a['uid'], HomePrefix, a['uid'], Host))
896 F.write("; Errors\n")
899 # Oops, something unspeakable happened.
905 def HostToIP(Host, mapped=True):
909 if Host[1].has_key("ipHostNumber"):
910 for addr in Host[1]["ipHostNumber"]:
911 IPAdresses.append(addr)
912 if IsV6Addr.match(addr) is None and mapped == "True":
913 IPAdresses.append("::ffff:"+addr)
917 # Generate the ssh known hosts file
918 def GenSSHKnown(host_attrs, File, mode=None):
921 OldMask = os.umask(0022)
922 F = open(File + ".tmp", "w", 0644)
926 if x[1].has_key("hostname") == 0 or \
927 x[1].has_key("sshRSAHostKey") == 0:
929 Host = GetAttr(x, "hostname")
931 if Host.endswith(HostDomain):
932 HostNames.append(Host[:-(len(HostDomain) + 1)])
934 # in the purpose field [[host|some other text]] (where some other text is optional)
935 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
936 # file. But so that we don't have to add everything we link we can add an asterisk
937 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
938 # http linking it we also support [[-hostname]] entries.
939 for i in x[1].get("purpose", []):
940 m = PurposeHostField.match(i)
943 # we ignore [[*..]] entries
944 if m.startswith('*'):
946 if m.startswith('-'):
950 if m.endswith(HostDomain):
951 HostNames.append(m[:-(len(HostDomain) + 1)])
953 for I in x[1]["sshRSAHostKey"]:
954 if mode and mode == 'authorized_keys':
956 if 'sshdistAuthKeysHost' in x[1]:
957 hosts += x[1]['sshdistAuthKeysHost']
958 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)
960 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
961 Line = Sanitize(Line) + "\n"
963 # Oops, something unspeakable happened.
969 # Generate the debianhosts file (list of all IP addresses)
970 def GenHosts(host_attrs, File):
973 OldMask = os.umask(0022)
974 F = open(File + ".tmp", "w", 0644)
981 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
984 if not 'ipHostNumber' in x[1]:
987 addrs = x[1]["ipHostNumber"]
991 addr = Sanitize(addr) + "\n"
994 # Oops, something unspeakable happened.
1000 def replaceTree(src, dst_basedir):
1001 bn = os.path.basename(src)
1002 dst = os.path.join(dst_basedir, bn)
1004 shutil.copytree(src, dst)
1006 def GenKeyrings(OutDir):
1008 if os.path.isdir(k):
1009 replaceTree(k, OutDir)
1011 shutil.copy(k, OutDir)
1014 def get_accounts(ldap_conn):
1015 # Fetch all the users
1016 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1017 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1018 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1019 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1020 "shadowExpire", "emailForward", "latitude", "longitude",\
1021 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1022 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1023 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1024 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1025 "mailContentInspectionAction", "webPassword"])
1027 if passwd_attrs is None:
1028 raise UDEmptyList, "No Users"
1029 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1030 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1034 def get_hosts(ldap_conn):
1035 # Fetch all the hosts
1036 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1037 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1038 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1040 if HostAttrs == None:
1041 raise UDEmptyList, "No Hosts"
1043 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1048 def make_ldap_conn():
1049 # Connect to the ldap server
1051 # for testing purposes it's sometimes useful to pass username/password
1052 # via the environment
1053 if 'UD_CREDENTIALS' in os.environ:
1054 Pass = os.environ['UD_CREDENTIALS'].split()
1056 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1057 Pass = F.readline().strip().split(" ")
1059 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1065 def setup_group_maps(l):
1066 # Fetch all the groups
1069 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1070 ["gid", "gidNumber", "subGroup"])
1072 # Generate the subgroup_map and group_id_map
1074 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1076 if x[1].has_key("gidNumber") == 0:
1078 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1079 if x[1].has_key("subGroup") != 0:
1080 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1084 SubGroupMap = subgroup_map
1085 GroupIDMap = group_id_map
1087 def generate_all(global_dir, ldap_conn):
1088 accounts = get_accounts(ldap_conn)
1089 host_attrs = get_hosts(ldap_conn)
1092 # Generate global things
1093 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1095 accounts = filter(lambda x: not IsRetired(x), accounts)
1096 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1098 CheckForward(accounts)
1100 GenMailDisable(accounts, global_dir + "mail-disable")
1101 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1102 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1103 GenPrivate(accounts, global_dir + "debian-private")
1104 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1105 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1106 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1107 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1108 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1109 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1110 GenWebPassword(accounts, global_dir + "web-passwords")
1111 GenKeyrings(global_dir)
1114 GenForward(accounts, global_dir + "forward-alias")
1116 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1117 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1119 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1120 GenMarkers(accounts, global_dir + "markers")
1121 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1122 GenHosts(host_attrs, global_dir + "debianhosts")
1123 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1125 GenDNS(accounts, global_dir + "dns-zone")
1126 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1128 setup_group_maps(ldap_conn)
1130 for host in host_attrs:
1131 if not "hostname" in host[1]:
1133 generate_host(host, global_dir, accounts, ssh_userkeys)
1135 def generate_host(host, global_dir, accounts, ssh_userkeys):
1136 current_host = host[1]['hostname'][0]
1137 OutDir = global_dir + current_host + '/'
1138 if not os.path.isdir(OutDir):
1141 # Get the group list and convert any named groups to numerics
1143 for groupname in AllowedGroupsPreload.strip().split(" "):
1144 GroupList[groupname] = True
1145 if 'allowedGroups' in host[1]:
1146 for groupname in host[1]['allowedGroups']:
1147 GroupList[groupname] = True
1148 for groupname in GroupList.keys():
1149 if groupname in GroupIDMap:
1150 GroupList[str(GroupIDMap[groupname])] = True
1153 if 'exportOptions' in host[1]:
1154 for extra in host[1]['exportOptions']:
1155 ExtraList[extra.upper()] = True
1158 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), accounts)
1160 DoLink(global_dir, OutDir, "debianhosts")
1161 DoLink(global_dir, OutDir, "ssh_known_hosts")
1162 DoLink(global_dir, OutDir, "disabled-accounts")
1165 if 'NOPASSWD' in ExtraList:
1166 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1168 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1170 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1171 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1173 # Now we know who we're allowing on the machine, export
1174 # the relevant ssh keys
1175 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1177 if not 'NOPASSWD' in ExtraList:
1178 GenShadow(accounts, OutDir + "shadow")
1180 # Link in global things
1181 if not 'NOMARKERS' in ExtraList:
1182 DoLink(global_dir, OutDir, "markers")
1183 DoLink(global_dir, OutDir, "mail-forward.cdb")
1184 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1185 DoLink(global_dir, OutDir, "mail-disable")
1186 DoLink(global_dir, OutDir, "mail-greylist")
1187 DoLink(global_dir, OutDir, "mail-callout")
1188 DoLink(global_dir, OutDir, "mail-rbl")
1189 DoLink(global_dir, OutDir, "mail-rhsbl")
1190 DoLink(global_dir, OutDir, "mail-whitelist")
1191 DoLink(global_dir, OutDir, "all-accounts.json")
1192 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1193 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1194 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1197 DoLink(global_dir, OutDir, "forward-alias")
1199 if 'DNS' in ExtraList:
1200 DoLink(global_dir, OutDir, "dns-zone")
1201 DoLink(global_dir, OutDir, "dns-sshfp")
1203 if 'AUTHKEYS' in ExtraList:
1204 DoLink(global_dir, OutDir, "authorized_keys")
1206 if 'BSMTP' in ExtraList:
1207 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1209 if 'PRIVATE' in ExtraList:
1210 DoLink(global_dir, OutDir, "debian-private")
1212 if 'GITOLITE' in ExtraList:
1213 DoLink(global_dir, OutDir, "ssh-gitolite")
1215 if 'WEB-PASSWORDS' in ExtraList:
1216 DoLink(global_dir, OutDir, "web-passwords")
1218 if 'KEYRING' in ExtraList:
1220 bn = os.path.basename(k)
1221 if os.path.isdir(k):
1222 src = os.path.join(global_dir, bn)
1223 replaceTree(src, OutDir)
1225 DoLink(global_dir, OutDir, bn)
1229 bn = os.path.basename(k)
1230 target = os.path.join(OutDir, bn)
1231 if os.path.isdir(target):
1234 posix.remove(target)
1237 DoLink(global_dir, OutDir, "last_update.trace")
1240 def getLastLDAPChangeTime(l):
1241 mods = l.search_s('cn=log',
1242 ldap.SCOPE_ONELEVEL,
1243 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1248 # Sort the list by reqEnd
1249 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1250 # Take the last element in the array
1251 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1255 def getLastBuildTime(gdir):
1259 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1260 cache_last_mod=fd.read().split()
1262 cache_last_mod = cache_last_mod[0]
1267 if e.errno == errno.ENOENT:
1272 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 generate_dir = os.environ['UD_GENERATEDIR']
1292 elif 'UD_GENERATEDIR' in os.environ:
1293 generate_dir = os.environ['UD_GENERATEDIR']
1295 ldap_last_mod = getLastLDAPChangeTime(l)
1296 cache_last_mod = getLastBuildTime(generate_dir)
1297 need_update = ldap_last_mod > cache_last_mod
1299 if not options.force and not need_update:
1300 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1301 fd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1307 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1308 lock = get_lock( lockf )
1310 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1313 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1314 generate_all(generate_dir, l)
1315 tracefd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1319 if lock is not None:
1322 if __name__ == "__main__":
1328 # vim:set shiftwidth=3: