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
364 safe_rmtree(os.path.join(global_dir, 'userkeys'))
365 safe_makedirs(os.path.join(global_dir, 'userkeys'))
368 if not 'sshRSAAuthKey' in a: continue
371 for I in a['sshRSAAuthKey']:
372 MultipleLine = "%s" % I
373 MultipleLine = Sanitize(MultipleLine)
374 contents.append(MultipleLine)
375 userkeys[a['uid']] = contents
378 # Generate the webPassword list
379 def GenWebPassword(accounts, File):
382 OldMask = os.umask(0077)
383 F = open(File, "w", 0600)
387 if not 'webPassword' in a: continue
388 if not a.pw_active(): continue
390 Pass = str(a['webPassword'])
391 Line = "%s:%s" % (a['uid'], Pass)
392 Line = Sanitize(Line) + "\n"
393 F.write("%s" % (Line))
399 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
400 OldMask = os.umask(0077)
401 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
404 if f not in ssh_userkeys:
406 # If we're not exporting their primary group, don't export
409 if userlist[f] in grouprevmap.keys():
410 grname = grouprevmap[userlist[f]]
413 if int(userlist[f]) <= 100:
414 # In these cases, look it up in the normal way so we
415 # deal with cases where, for instance, users are in group
416 # users as their primary group.
417 grname = grp.getgrgid(userlist[f])[0]
422 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])
426 for line in ssh_userkeys[f]:
427 if line.startswith("allowed_hosts=") and ' ' in line:
428 machines, line = line.split('=', 1)[1].split(' ', 1)
429 if current_host not in machines.split(','):
430 continue # skip this key
433 continue # no keys for this host
434 contents = "\n".join(lines) + "\n"
436 to = tarfile.TarInfo(name=f)
437 # These will only be used where the username doesn't
438 # exist on the target system for some reason; hence,
439 # in those cases, the safest thing is for the file to
440 # be owned by root but group nobody. This deals with
441 # the bloody obscure case where the group fails to exist
442 # whilst the user does (in which case we want to avoid
443 # ending up with a file which is owned user:root to avoid
444 # a fairly obvious attack vector)
447 # Using the username / groupname fields avoids any need
448 # to give a shit^W^W^Wcare about the UIDoffset stuff.
452 to.mtime = int(time.time())
453 to.size = len(contents)
455 tf.addfile(to, StringIO(contents))
458 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
460 # add a list of groups to existing groups,
461 # including all subgroups thereof, recursively.
462 # basically this proceduces the transitive hull of the groups in
464 def addGroups(existingGroups, newGroups, uid, current_host):
465 for group in newGroups:
466 # if it's a <group>@host, split it and verify it's on the current host.
467 s = group.split('@', 1)
468 if len(s) == 2 and s[1] != current_host:
472 # let's see if we handled this group already
473 if group in existingGroups:
476 if not GroupIDMap.has_key(group):
477 print "Group", group, "does not exist but", uid, "is in it"
480 existingGroups.append(group)
482 if SubGroupMap.has_key(group):
483 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
485 # Generate the group list
486 def GenGroup(accounts, File, current_host):
490 F = open(File + ".tdb.tmp", "w")
492 # Generate the GroupMap
496 GroupHasPrimaryMembers = {}
498 # Sort them into a list of groups having a set of users
500 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
501 if not 'supplementaryGid' in a: continue
504 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
506 GroupMap[g].append(a['uid'])
508 # Output the group file.
510 for x in GroupMap.keys():
511 if not x in GroupIDMap:
514 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
517 grouprevmap[GroupIDMap[x]] = x
519 Line = "%s:x:%u:" % (x, GroupIDMap[x])
521 for I in GroupMap[x]:
522 Line = Line + ("%s%s" % (Comma, I))
524 Line = Sanitize(Line) + "\n"
525 F.write("0%u %s" % (J, Line))
526 F.write(".%s %s" % (x, Line))
527 F.write("=%u %s" % (GroupIDMap[x], Line))
530 # Oops, something unspeakable happened.
538 def CheckForward(accounts):
540 if not 'emailForward' in a: continue
544 # Do not allow people to try to buffer overflow busted parsers
545 if len(a['emailForward']) > 200: delete = True
546 # Check the forwarding address
547 elif EmailCheck.match(a['emailForward']) is None: delete = True
550 a.delete_mailforward()
552 # Generate the email forwarding list
553 def GenForward(accounts, File):
556 OldMask = os.umask(0022)
557 F = open(File + ".tmp", "w", 0644)
561 if not 'emailForward' in a: continue
562 Line = "%s: %s" % (a['uid'], a['emailForward'])
563 Line = Sanitize(Line) + "\n"
566 # Oops, something unspeakable happened.
572 def GenCDB(accounts, File, key):
575 OldMask = os.umask(0022)
576 Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
579 # Write out the email address for each user
581 if not key in a: continue
584 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
587 # Oops, something unspeakable happened.
591 if Fdb.close() != None:
592 raise "cdbmake gave an error"
594 # Generate the anon XEarth marker file
595 def GenMarkers(accounts, File):
598 F = open(File + ".tmp", "w")
600 # Write out the position for each user
602 if not ('latitude' in a and 'longitude' in a): continue
604 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
605 Line = Sanitize(Line) + "\n"
610 # Oops, something unspeakable happened.
616 # Generate the debian-private subscription list
617 def GenPrivate(accounts, File):
620 F = open(File + ".tmp", "w")
622 # Write out the position for each user
624 if not a.is_active_user(): continue
625 if not 'privateSub' in a: continue
627 Line = "%s"%(a['privateSub'])
628 Line = Sanitize(Line) + "\n"
633 # Oops, something unspeakable happened.
639 # Generate a list of locked accounts
640 def GenDisabledAccounts(accounts, File):
643 F = open(File + ".tmp", "w")
644 disabled_accounts = []
646 # Fetch all the users
648 if a.pw_active(): continue
649 Line = "%s:%s" % (a['uid'], "Account is locked")
650 disabled_accounts.append(a)
651 F.write(Sanitize(Line) + "\n")
653 # Oops, something unspeakable happened.
658 return disabled_accounts
660 # Generate the list of local addresses that refuse all mail
661 def GenMailDisable(accounts, File):
664 F = open(File + ".tmp", "w")
667 if not 'mailDisableMessage' in a: continue
668 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
669 Line = Sanitize(Line) + "\n"
672 # Oops, something unspeakable happened.
678 # Generate a list of uids that should have boolean affects applied
679 def GenMailBool(accounts, File, key):
682 F = open(File + ".tmp", "w")
685 if not key in a: continue
686 if not a[key] == 'TRUE': continue
687 Line = "%s"%(a['uid'])
688 Line = Sanitize(Line) + "\n"
691 # Oops, something unspeakable happened.
697 # Generate a list of hosts for RBL or whitelist purposes.
698 def GenMailList(accounts, File, key):
701 F = open(File + ".tmp", "w")
703 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
704 else: validregex = re.compile('^[-\w.]+$')
707 if not key in a: continue
709 filtered = filter(lambda z: validregex.match(z), a[key])
710 if len(filtered) == 0: continue
711 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
712 line = a['uid'] + ': ' + ' : '.join(filtered)
713 line = Sanitize(line) + "\n"
716 # Oops, something unspeakable happened.
722 def isRoleAccount(account):
723 return 'debianRoleAccount' in account['objectClass']
725 # Generate the DNS Zone file
726 def GenDNS(accounts, File):
729 F = open(File + ".tmp", "w")
731 # Fetch all the users
734 # Write out the zone file entry for each user
736 if not 'dnsZoneEntry' in a: continue
737 if not a.is_active_user() and not isRoleAccount(a): continue
740 F.write("; %s\n"%(a.email_address()))
741 for z in a["dnsZoneEntry"]:
742 Split = z.lower().split()
743 if Split[1].lower() == 'in':
744 Line = " ".join(Split) + "\n"
747 Host = Split[0] + DNSZone
748 if BSMTPCheck.match(Line) != None:
749 F.write("; Has BSMTP\n")
751 # Write some identification information
752 if not RRs.has_key(Host):
753 if Split[2].lower() in ["a", "aaaa"]:
754 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
755 for y in a["keyFingerPrint"]:
756 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
760 Line = "; Err %s"%(str(Split))
765 F.write("; Errors:\n")
766 for line in str(e).split("\n"):
767 F.write("; %s\n"%(line))
770 # Oops, something unspeakable happened.
776 def ExtractDNSInfo(x):
780 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
783 if x[1].has_key("ipHostNumber"):
784 for I in x[1]["ipHostNumber"]:
785 if IsV6Addr.match(I) != None:
786 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
788 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
792 if 'sshRSAHostKey' in x[1]:
793 for I in x[1]["sshRSAHostKey"]:
795 if Split[0] == 'ssh-rsa':
797 if Split[0] == 'ssh-dss':
799 if Algorithm == None:
801 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
802 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
804 if 'architecture' in x[1]:
805 Arch = GetAttr(x, "architecture")
807 if x[1].has_key("machine"):
808 Mach = " " + GetAttr(x, "machine")
809 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
811 if x[1].has_key("mXRecord"):
812 for I in x[1]["mXRecord"]:
813 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
817 # Generate the DNS records
818 def GenZoneRecords(host_attrs, File):
821 F = open(File + ".tmp", "w")
823 # Fetch all the hosts
825 if x[1].has_key("hostname") == 0:
828 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
831 DNSInfo = ExtractDNSInfo(x)
835 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
838 Line = "\t\t\t%s" % (Line)
842 # this would write sshfp lines for services on machines
843 # but we can't yet, since some are cnames and we'll make
844 # an invalid zonefile
846 # for i in x[1].get("purpose", []):
847 # m = PurposeHostField.match(i)
850 # # we ignore [[*..]] entries
851 # if m.startswith('*'):
853 # if m.startswith('-'):
856 # if not m.endswith(HostDomain):
858 # if not m.endswith('.'):
860 # for Line in DNSInfo:
861 # if isSSHFP.match(Line):
862 # Line = "%s\t%s" % (m, Line)
863 # F.write(Line + "\n")
865 # Oops, something unspeakable happened.
871 # Generate the BSMTP file
872 def GenBSMTP(accounts, File, HomePrefix):
875 F = open(File + ".tmp", "w")
877 # Write out the zone file entry for each user
879 if not 'dnsZoneEntry' in a: continue
880 if not a.is_active_user(): continue
883 for z in a["dnsZoneEntry"]:
884 Split = z.lower().split()
885 if Split[1].lower() == 'in':
886 for y in range(0, len(Split)):
889 Line = " ".join(Split) + "\n"
891 Host = Split[0] + DNSZone
892 if BSMTPCheck.match(Line) != None:
893 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
894 a['uid'], HomePrefix, a['uid'], Host))
897 F.write("; Errors\n")
900 # Oops, something unspeakable happened.
906 def HostToIP(Host, mapped=True):
910 if Host[1].has_key("ipHostNumber"):
911 for addr in Host[1]["ipHostNumber"]:
912 IPAdresses.append(addr)
913 if IsV6Addr.match(addr) is None and mapped == "True":
914 IPAdresses.append("::ffff:"+addr)
918 # Generate the ssh known hosts file
919 def GenSSHKnown(host_attrs, File, mode=None):
922 OldMask = os.umask(0022)
923 F = open(File + ".tmp", "w", 0644)
927 if x[1].has_key("hostname") == 0 or \
928 x[1].has_key("sshRSAHostKey") == 0:
930 Host = GetAttr(x, "hostname")
932 if Host.endswith(HostDomain):
933 HostNames.append(Host[:-(len(HostDomain) + 1)])
935 # in the purpose field [[host|some other text]] (where some other text is optional)
936 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
937 # file. But so that we don't have to add everything we link we can add an asterisk
938 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
939 # http linking it we also support [[-hostname]] entries.
940 for i in x[1].get("purpose", []):
941 m = PurposeHostField.match(i)
944 # we ignore [[*..]] entries
945 if m.startswith('*'):
947 if m.startswith('-'):
951 if m.endswith(HostDomain):
952 HostNames.append(m[:-(len(HostDomain) + 1)])
954 for I in x[1]["sshRSAHostKey"]:
955 if mode and mode == 'authorized_keys':
957 if 'sshdistAuthKeysHost' in x[1]:
958 hosts += x[1]['sshdistAuthKeysHost']
959 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)
961 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
962 Line = Sanitize(Line) + "\n"
964 # Oops, something unspeakable happened.
970 # Generate the debianhosts file (list of all IP addresses)
971 def GenHosts(host_attrs, File):
974 OldMask = os.umask(0022)
975 F = open(File + ".tmp", "w", 0644)
982 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
985 if not 'ipHostNumber' in x[1]:
988 addrs = x[1]["ipHostNumber"]
992 addr = Sanitize(addr) + "\n"
995 # Oops, something unspeakable happened.
1001 def replaceTree(src, dst_basedir):
1002 bn = os.path.basename(src)
1003 dst = os.path.join(dst_basedir, bn)
1005 shutil.copytree(src, dst)
1007 def GenKeyrings(OutDir):
1009 if os.path.isdir(k):
1010 replaceTree(k, OutDir)
1012 shutil.copy(k, OutDir)
1015 def get_accounts(ldap_conn):
1016 # Fetch all the users
1017 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1018 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1019 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1020 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1021 "shadowExpire", "emailForward", "latitude", "longitude",\
1022 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1023 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1024 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1025 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1026 "mailContentInspectionAction", "webPassword"])
1028 if passwd_attrs is None:
1029 raise UDEmptyList, "No Users"
1030 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1031 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1035 def get_hosts(ldap_conn):
1036 # Fetch all the hosts
1037 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1038 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1039 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1041 if HostAttrs == None:
1042 raise UDEmptyList, "No Hosts"
1044 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1049 def make_ldap_conn():
1050 # Connect to the ldap server
1052 # for testing purposes it's sometimes useful to pass username/password
1053 # via the environment
1054 if 'UD_CREDENTIALS' in os.environ:
1055 Pass = os.environ['UD_CREDENTIALS'].split()
1057 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1058 Pass = F.readline().strip().split(" ")
1060 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1066 def setup_group_maps(l):
1067 # Fetch all the groups
1070 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1071 ["gid", "gidNumber", "subGroup"])
1073 # Generate the subgroup_map and group_id_map
1075 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1077 if x[1].has_key("gidNumber") == 0:
1079 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1080 if x[1].has_key("subGroup") != 0:
1081 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1085 SubGroupMap = subgroup_map
1086 GroupIDMap = group_id_map
1088 def generate_all(global_dir, ldap_conn):
1089 accounts = get_accounts(ldap_conn)
1090 host_attrs = get_hosts(ldap_conn)
1093 # Generate global things
1094 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1096 accounts = filter(lambda x: not IsRetired(x), accounts)
1097 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1099 CheckForward(accounts)
1101 GenMailDisable(accounts, global_dir + "mail-disable")
1102 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1103 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1104 GenPrivate(accounts, global_dir + "debian-private")
1105 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1106 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1107 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1108 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1109 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1110 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1111 GenWebPassword(accounts, global_dir + "web-passwords")
1112 GenKeyrings(global_dir)
1115 GenForward(accounts, global_dir + "forward-alias")
1117 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1118 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1120 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1121 GenMarkers(accounts, global_dir + "markers")
1122 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1123 GenHosts(host_attrs, global_dir + "debianhosts")
1124 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1126 GenDNS(accounts, global_dir + "dns-zone")
1127 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1129 setup_group_maps(ldap_conn)
1131 for host in host_attrs:
1132 if not "hostname" in host[1]:
1134 generate_host(host, global_dir, accounts, ssh_userkeys)
1136 def generate_host(host, global_dir, accounts, ssh_userkeys):
1137 current_host = host[1]['hostname'][0]
1138 OutDir = global_dir + current_host + '/'
1139 if not os.path.isdir(OutDir):
1142 # Get the group list and convert any named groups to numerics
1144 for groupname in AllowedGroupsPreload.strip().split(" "):
1145 GroupList[groupname] = True
1146 if 'allowedGroups' in host[1]:
1147 for groupname in host[1]['allowedGroups']:
1148 GroupList[groupname] = True
1149 for groupname in GroupList.keys():
1150 if groupname in GroupIDMap:
1151 GroupList[str(GroupIDMap[groupname])] = True
1154 if 'exportOptions' in host[1]:
1155 for extra in host[1]['exportOptions']:
1156 ExtraList[extra.upper()] = True
1159 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), accounts)
1161 DoLink(global_dir, OutDir, "debianhosts")
1162 DoLink(global_dir, OutDir, "ssh_known_hosts")
1163 DoLink(global_dir, OutDir, "disabled-accounts")
1166 if 'NOPASSWD' in ExtraList:
1167 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1169 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1171 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1172 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1174 # Now we know who we're allowing on the machine, export
1175 # the relevant ssh keys
1176 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1178 if not 'NOPASSWD' in ExtraList:
1179 GenShadow(accounts, OutDir + "shadow")
1181 # Link in global things
1182 if not 'NOMARKERS' in ExtraList:
1183 DoLink(global_dir, OutDir, "markers")
1184 DoLink(global_dir, OutDir, "mail-forward.cdb")
1185 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1186 DoLink(global_dir, OutDir, "mail-disable")
1187 DoLink(global_dir, OutDir, "mail-greylist")
1188 DoLink(global_dir, OutDir, "mail-callout")
1189 DoLink(global_dir, OutDir, "mail-rbl")
1190 DoLink(global_dir, OutDir, "mail-rhsbl")
1191 DoLink(global_dir, OutDir, "mail-whitelist")
1192 DoLink(global_dir, OutDir, "all-accounts.json")
1193 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1194 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1195 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1198 DoLink(global_dir, OutDir, "forward-alias")
1200 if 'DNS' in ExtraList:
1201 DoLink(global_dir, OutDir, "dns-zone")
1202 DoLink(global_dir, OutDir, "dns-sshfp")
1204 if 'AUTHKEYS' in ExtraList:
1205 DoLink(global_dir, OutDir, "authorized_keys")
1207 if 'BSMTP' in ExtraList:
1208 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1210 if 'PRIVATE' in ExtraList:
1211 DoLink(global_dir, OutDir, "debian-private")
1213 if 'GITOLITE' in ExtraList:
1214 DoLink(global_dir, OutDir, "ssh-gitolite")
1216 if 'WEB-PASSWORDS' in ExtraList:
1217 DoLink(global_dir, OutDir, "web-passwords")
1219 if 'KEYRING' in ExtraList:
1221 bn = os.path.basename(k)
1222 if os.path.isdir(k):
1223 src = os.path.join(global_dir, bn)
1224 replaceTree(src, OutDir)
1226 DoLink(global_dir, OutDir, bn)
1230 bn = os.path.basename(k)
1231 target = os.path.join(OutDir, bn)
1232 if os.path.isdir(target):
1235 posix.remove(target)
1238 DoLink(global_dir, OutDir, "last_update.trace")
1241 def getLastLDAPChangeTime(l):
1242 mods = l.search_s('cn=log',
1243 ldap.SCOPE_ONELEVEL,
1244 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1249 # Sort the list by reqEnd
1250 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1251 # Take the last element in the array
1252 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1256 def getLastBuildTime(gdir):
1260 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1261 cache_last_mod=fd.read().split()
1263 cache_last_mod = cache_last_mod[0]
1268 if e.errno == errno.ENOENT:
1273 return cache_last_mod
1277 parser = optparse.OptionParser()
1278 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1279 help="Output directory.")
1280 parser.add_option("-f", "--force", dest="force", action="store_true",
1281 help="Force generation, even if not update to LDAP has happened.")
1283 (options, args) = parser.parse_args()
1289 l = make_ldap_conn()
1291 if options.generatedir is not None:
1292 generate_dir = os.environ['UD_GENERATEDIR']
1293 elif 'UD_GENERATEDIR' in os.environ:
1294 generate_dir = os.environ['UD_GENERATEDIR']
1296 ldap_last_mod = getLastLDAPChangeTime(l)
1297 cache_last_mod = getLastBuildTime(generate_dir)
1298 need_update = ldap_last_mod > cache_last_mod
1300 if not options.force and not need_update:
1301 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1302 fd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1308 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1309 lock = get_lock( lockf )
1311 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1314 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1315 generate_all(generate_dir, l)
1316 tracefd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1320 if lock is not None:
1323 if __name__ == "__main__":
1329 # vim:set shiftwidth=3: