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
372 for I in a['sshRSAAuthKey']:
373 MultipleLine = "%s" % I
374 MultipleLine = Sanitize(MultipleLine)
375 contents.append(MultipleLine)
376 userkeys[a['uid']] = contents
379 # Generate the webPassword list
380 def GenWebPassword(accounts, File):
383 OldMask = os.umask(0077)
384 F = open(File, "w", 0600)
388 if not 'webPassword' in a: continue
389 if not a.pw_active(): continue
391 Pass = str(a['webPassword'])
392 Line = "%s:%s" % (a['uid'], Pass)
393 Line = Sanitize(Line) + "\n"
394 F.write("%s" % (Line))
400 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target):
401 OldMask = os.umask(0077)
402 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
405 if f not in ssh_userkeys:
407 # If we're not exporting their primary group, don't export
410 if userlist[f] in grouprevmap.keys():
411 grname = grouprevmap[userlist[f]]
414 if int(userlist[f]) <= 100:
415 # In these cases, look it up in the normal way so we
416 # deal with cases where, for instance, users are in group
417 # users as their primary group.
418 grname = grp.getgrgid(userlist[f])[0]
423 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])
427 for line in ssh_userkeys[f]:
428 if line.startswith("allowed_hosts=") and ' ' in line:
429 machines, line = line.split('=', 1)[1].split(' ', 1)
430 if CurrentHost not in machines.split(','):
431 continue # skip this key
434 continue # no keys for this host
435 contents = "\n".join(lines) + "\n"
437 to = tarfile.TarInfo(name=f)
438 # These will only be used where the username doesn't
439 # exist on the target system for some reason; hence,
440 # in those cases, the safest thing is for the file to
441 # be owned by root but group nobody. This deals with
442 # the bloody obscure case where the group fails to exist
443 # whilst the user does (in which case we want to avoid
444 # ending up with a file which is owned user:root to avoid
445 # a fairly obvious attack vector)
448 # Using the username / groupname fields avoids any need
449 # to give a shit^W^W^Wcare about the UIDoffset stuff.
453 to.mtime = int(time.time())
454 to.size = len(contents)
456 tf.addfile(to, StringIO(contents))
459 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
461 # add a list of groups to existing groups,
462 # including all subgroups thereof, recursively.
463 # basically this proceduces the transitive hull of the groups in
465 def addGroups(existingGroups, newGroups, uid):
466 for group in newGroups:
467 # if it's a <group>@host, split it and verify it's on the current host.
468 s = group.split('@', 1)
469 if len(s) == 2 and s[1] != CurrentHost:
473 # let's see if we handled this group already
474 if group in existingGroups:
477 if not GroupIDMap.has_key(group):
478 print "Group", group, "does not exist but", uid, "is in it"
481 existingGroups.append(group)
483 if SubGroupMap.has_key(group):
484 addGroups(existingGroups, SubGroupMap[group], uid)
486 # Generate the group list
487 def GenGroup(accounts, File):
491 F = open(File + ".tdb.tmp", "w")
493 # Generate the GroupMap
495 for x in GroupIDMap.keys():
497 GroupHasPrimaryMembers = {}
499 # Sort them into a list of groups having a set of users
501 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
502 if not 'supplementaryGid' in a: continue
505 addGroups(supgroups, a['supplementaryGid'], a['uid'])
507 GroupMap[g].append(a['uid'])
509 # Output the group file.
511 for x in GroupMap.keys():
512 if GroupIDMap.has_key(x) == 0:
515 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
518 grouprevmap[GroupIDMap[x]] = x
520 Line = "%s:x:%u:" % (x, GroupIDMap[x])
522 for I in GroupMap[x]:
523 Line = Line + ("%s%s" % (Comma, I))
525 Line = Sanitize(Line) + "\n"
526 F.write("0%u %s" % (J, Line))
527 F.write(".%s %s" % (x, Line))
528 F.write("=%u %s" % (GroupIDMap[x], Line))
531 # Oops, something unspeakable happened.
539 def CheckForward(accounts):
541 if not 'emailForward' in a: continue
545 # Do not allow people to try to buffer overflow busted parsers
546 if len(a['emailForward']) > 200: delete = True
547 # Check the forwarding address
548 elif EmailCheck.match(a['emailForward']) is None: delete = True
551 a.delete_mailforward()
553 # Generate the email forwarding list
554 def GenForward(accounts, File):
557 OldMask = os.umask(0022)
558 F = open(File + ".tmp", "w", 0644)
562 if not 'emailForward' in a: continue
563 Line = "%s: %s" % (a['uid'], a['emailForward'])
564 Line = Sanitize(Line) + "\n"
567 # Oops, something unspeakable happened.
573 def GenCDB(accounts, File, key):
576 OldMask = os.umask(0022)
577 Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
580 # Write out the email address for each user
582 if not key in a: continue
585 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
588 # Oops, something unspeakable happened.
592 if Fdb.close() != None:
593 raise "cdbmake gave an error"
595 # Generate the anon XEarth marker file
596 def GenMarkers(accounts, File):
599 F = open(File + ".tmp", "w")
601 # Write out the position for each user
603 if not ('latitude' in a and 'longitude' in a): continue
605 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
606 Line = Sanitize(Line) + "\n"
611 # Oops, something unspeakable happened.
617 # Generate the debian-private subscription list
618 def GenPrivate(accounts, File):
621 F = open(File + ".tmp", "w")
623 # Write out the position for each user
625 if not a.is_active_user(): continue
626 if not 'privateSub' in a: continue
628 Line = "%s"%(a['privateSub'])
629 Line = Sanitize(Line) + "\n"
634 # Oops, something unspeakable happened.
640 # Generate a list of locked accounts
641 def GenDisabledAccounts(accounts, File):
644 F = open(File + ".tmp", "w")
645 disabled_accounts = []
647 # Fetch all the users
649 if a.pw_active(): continue
650 Line = "%s:%s" % (a['uid'], "Account is locked")
651 disabled_accounts.append(a)
652 F.write(Sanitize(Line) + "\n")
654 # Oops, something unspeakable happened.
659 return disabled_accounts
661 # Generate the list of local addresses that refuse all mail
662 def GenMailDisable(accounts, File):
665 F = open(File + ".tmp", "w")
668 if not 'mailDisableMessage' in a: continue
669 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
670 Line = Sanitize(Line) + "\n"
673 # Oops, something unspeakable happened.
679 # Generate a list of uids that should have boolean affects applied
680 def GenMailBool(accounts, File, key):
683 F = open(File + ".tmp", "w")
686 if not key in a: continue
687 if not a[key] == 'TRUE': continue
688 Line = "%s"%(a['uid'])
689 Line = Sanitize(Line) + "\n"
692 # Oops, something unspeakable happened.
698 # Generate a list of hosts for RBL or whitelist purposes.
699 def GenMailList(accounts, File, key):
702 F = open(File + ".tmp", "w")
704 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
705 else: validregex = re.compile('^[-\w.]+$')
708 if not key in a: continue
710 filtered = filter(lambda z: validregex.match(z), a[key])
711 if len(filtered) == 0: continue
712 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
713 line = a['uid'] + ': ' + ' : '.join(filtered)
714 line = Sanitize(line) + "\n"
717 # Oops, something unspeakable happened.
723 def isRoleAccount(account):
724 return 'debianRoleAccount' in account['objectClass']
726 # Generate the DNS Zone file
727 def GenDNS(accounts, File):
730 F = open(File + ".tmp", "w")
732 # Fetch all the users
735 # Write out the zone file entry for each user
737 if not 'dnsZoneEntry' in a: continue
738 if not a.is_active_user() and not isRoleAccount(a): continue
741 F.write("; %s\n"%(a.email_address()))
742 for z in a["dnsZoneEntry"]:
743 Split = z.lower().split()
744 if Split[1].lower() == 'in':
745 Line = " ".join(Split) + "\n"
748 Host = Split[0] + DNSZone
749 if BSMTPCheck.match(Line) != None:
750 F.write("; Has BSMTP\n")
752 # Write some identification information
753 if not RRs.has_key(Host):
754 if Split[2].lower() in ["a", "aaaa"]:
755 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
756 for y in a["keyFingerPrint"]:
757 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
761 Line = "; Err %s"%(str(Split))
766 F.write("; Errors:\n")
767 for line in str(e).split("\n"):
768 F.write("; %s\n"%(line))
771 # Oops, something unspeakable happened.
777 def ExtractDNSInfo(x):
781 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
784 if x[1].has_key("ipHostNumber"):
785 for I in x[1]["ipHostNumber"]:
786 if IsV6Addr.match(I) != None:
787 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
789 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
793 if 'sshRSAHostKey' in x[1]:
794 for I in x[1]["sshRSAHostKey"]:
796 if Split[0] == 'ssh-rsa':
798 if Split[0] == 'ssh-dss':
800 if Algorithm == None:
802 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
803 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
805 if 'architecture' in x[1]:
806 Arch = GetAttr(x, "architecture")
808 if x[1].has_key("machine"):
809 Mach = " " + GetAttr(x, "machine")
810 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
812 if x[1].has_key("mXRecord"):
813 for I in x[1]["mXRecord"]:
814 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
818 # Generate the DNS records
819 def GenZoneRecords(host_attrs, File):
822 F = open(File + ".tmp", "w")
824 # Fetch all the hosts
826 if x[1].has_key("hostname") == 0:
829 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
832 DNSInfo = ExtractDNSInfo(x)
836 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
839 Line = "\t\t\t%s" % (Line)
843 # this would write sshfp lines for services on machines
844 # but we can't yet, since some are cnames and we'll make
845 # an invalid zonefile
847 # for i in x[1].get("purpose", []):
848 # m = PurposeHostField.match(i)
851 # # we ignore [[*..]] entries
852 # if m.startswith('*'):
854 # if m.startswith('-'):
857 # if not m.endswith(HostDomain):
859 # if not m.endswith('.'):
861 # for Line in DNSInfo:
862 # if isSSHFP.match(Line):
863 # Line = "%s\t%s" % (m, Line)
864 # F.write(Line + "\n")
866 # Oops, something unspeakable happened.
872 # Generate the BSMTP file
873 def GenBSMTP(accounts, File, HomePrefix):
876 F = open(File + ".tmp", "w")
878 # Write out the zone file entry for each user
880 if not 'dnsZoneEntry' in a: continue
881 if not a.is_active_user(): continue
884 for z in a["dnsZoneEntry"]:
885 Split = z.lower().split()
886 if Split[1].lower() == 'in':
887 for y in range(0, len(Split)):
890 Line = " ".join(Split) + "\n"
892 Host = Split[0] + DNSZone
893 if BSMTPCheck.match(Line) != None:
894 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
895 a['uid'], HomePrefix, a['uid'], Host))
898 F.write("; Errors\n")
901 # Oops, something unspeakable happened.
907 def HostToIP(Host, mapped=True):
911 if Host[1].has_key("ipHostNumber"):
912 for addr in Host[1]["ipHostNumber"]:
913 IPAdresses.append(addr)
914 if IsV6Addr.match(addr) is None and mapped == "True":
915 IPAdresses.append("::ffff:"+addr)
919 # Generate the ssh known hosts file
920 def GenSSHKnown(host_attrs, File, mode=None):
923 OldMask = os.umask(0022)
924 F = open(File + ".tmp", "w", 0644)
928 if x[1].has_key("hostname") == 0 or \
929 x[1].has_key("sshRSAHostKey") == 0:
931 Host = GetAttr(x, "hostname")
933 if Host.endswith(HostDomain):
934 HostNames.append(Host[:-(len(HostDomain) + 1)])
936 # in the purpose field [[host|some other text]] (where some other text is optional)
937 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
938 # file. But so that we don't have to add everything we link we can add an asterisk
939 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
940 # http linking it we also support [[-hostname]] entries.
941 for i in x[1].get("purpose", []):
942 m = PurposeHostField.match(i)
945 # we ignore [[*..]] entries
946 if m.startswith('*'):
948 if m.startswith('-'):
952 if m.endswith(HostDomain):
953 HostNames.append(m[:-(len(HostDomain) + 1)])
955 for I in x[1]["sshRSAHostKey"]:
956 if mode and mode == 'authorized_keys':
958 if 'sshdistAuthKeysHost' in x[1]:
959 hosts += x[1]['sshdistAuthKeysHost']
960 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)
962 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
963 Line = Sanitize(Line) + "\n"
965 # Oops, something unspeakable happened.
971 # Generate the debianhosts file (list of all IP addresses)
972 def GenHosts(host_attrs, File):
975 OldMask = os.umask(0022)
976 F = open(File + ".tmp", "w", 0644)
983 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
986 if not 'ipHostNumber' in x[1]:
989 addrs = x[1]["ipHostNumber"]
993 addr = Sanitize(addr) + "\n"
996 # Oops, something unspeakable happened.
1002 def replaceTree(src, dst_basedir):
1003 bn = os.path.basename(src)
1004 dst = os.path.join(dst_basedir, bn)
1006 shutil.copytree(src, dst)
1008 def GenKeyrings(OutDir):
1010 if os.path.isdir(k):
1011 replaceTree(k, OutDir)
1013 shutil.copy(k, OutDir)
1016 def get_accounts(ldap_conn):
1017 # Fetch all the users
1018 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1019 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1020 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1021 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1022 "shadowExpire", "emailForward", "latitude", "longitude",\
1023 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1024 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1025 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1026 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1027 "mailContentInspectionAction", "webPassword"])
1029 if passwd_attrs is None:
1030 raise UDEmptyList, "No Users"
1031 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1032 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1036 def get_hosts(ldap_conn):
1037 # Fetch all the hosts
1038 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1039 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1040 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1042 if HostAttrs == None:
1043 raise UDEmptyList, "No Hosts"
1045 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1050 def make_ldap_conn():
1051 # Connect to the ldap server
1053 # for testing purposes it's sometimes useful to pass username/password
1054 # via the environment
1055 if 'UD_CREDENTIALS' in os.environ:
1056 Pass = os.environ['UD_CREDENTIALS'].split()
1058 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1059 Pass = F.readline().strip().split(" ")
1061 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1065 def generate_all(global_dir, ldap_conn):
1066 accounts = get_accounts(ldap_conn)
1067 host_attrs = get_hosts(ldap_conn)
1070 # Generate global things
1071 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1073 accounts = filter(lambda x: not IsRetired(x), accounts)
1074 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1076 CheckForward(accounts)
1078 GenMailDisable(accounts, global_dir + "mail-disable")
1079 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1080 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1081 GenPrivate(accounts, global_dir + "debian-private")
1082 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1083 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1084 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1085 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1086 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1087 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1088 GenWebPassword(accounts, global_dir + "web-passwords")
1089 GenKeyrings(global_dir)
1092 GenForward(accounts, global_dir + "forward-alias")
1094 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1095 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1097 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1098 GenMarkers(accounts, global_dir + "markers")
1099 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1100 GenHosts(host_attrs, global_dir + "debianhosts")
1101 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1103 GenDNS(accounts, global_dir + "dns-zone")
1104 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1106 for host in host_attrs:
1107 if not "hostname" in host[1]:
1109 generate_host(host, global_dir, accounts, ssh_userkeys)
1111 def generate_host(host, global_dir, accounts, ssh_userkeys):
1114 CurrentHost = host[1]['hostname'][0]
1115 OutDir = global_dir + CurrentHost + '/'
1121 # Get the group list and convert any named groups to numerics
1123 for groupname in AllowedGroupsPreload.strip().split(" "):
1124 GroupList[groupname] = True
1125 if 'allowedGroups' in host[1]:
1126 for groupname in host[1]['allowedGroups']:
1127 GroupList[groupname] = True
1128 for groupname in GroupList.keys():
1129 if groupname in GroupIDMap:
1130 GroupList[str(GroupIDMap[groupname])] = True
1133 if 'exportOptions' in host[1]:
1134 for extra in host[1]['exportOptions']:
1135 ExtraList[extra.upper()] = True
1138 accounts = filter(lambda x: IsInGroup(x, GroupList), accounts)
1140 DoLink(global_dir, OutDir, "debianhosts")
1141 DoLink(global_dir, OutDir, "ssh_known_hosts")
1142 DoLink(global_dir, OutDir, "disabled-accounts")
1145 if 'NOPASSWD' in ExtraList:
1146 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1148 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1150 grouprevmap = GenGroup(accounts, OutDir + "group")
1151 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1153 # Now we know who we're allowing on the machine, export
1154 # the relevant ssh keys
1155 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1157 if not 'NOPASSWD' in ExtraList:
1158 GenShadow(accounts, OutDir + "shadow")
1160 # Link in global things
1161 if not 'NOMARKERS' in ExtraList:
1162 DoLink(global_dir, OutDir, "markers")
1163 DoLink(global_dir, OutDir, "mail-forward.cdb")
1164 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1165 DoLink(global_dir, OutDir, "mail-disable")
1166 DoLink(global_dir, OutDir, "mail-greylist")
1167 DoLink(global_dir, OutDir, "mail-callout")
1168 DoLink(global_dir, OutDir, "mail-rbl")
1169 DoLink(global_dir, OutDir, "mail-rhsbl")
1170 DoLink(global_dir, OutDir, "mail-whitelist")
1171 DoLink(global_dir, OutDir, "all-accounts.json")
1172 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1173 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1174 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1177 DoLink(global_dir, OutDir, "forward-alias")
1179 if 'DNS' in ExtraList:
1180 DoLink(global_dir, OutDir, "dns-zone")
1181 DoLink(global_dir, OutDir, "dns-sshfp")
1183 if 'AUTHKEYS' in ExtraList:
1184 DoLink(global_dir, OutDir, "authorized_keys")
1186 if 'BSMTP' in ExtraList:
1187 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1189 if 'PRIVATE' in ExtraList:
1190 DoLink(global_dir, OutDir, "debian-private")
1192 if 'GITOLITE' in ExtraList:
1193 DoLink(global_dir, OutDir, "ssh-gitolite")
1195 if 'WEB-PASSWORDS' in ExtraList:
1196 DoLink(global_dir, OutDir, "web-passwords")
1198 if 'KEYRING' in ExtraList:
1200 bn = os.path.basename(k)
1201 if os.path.isdir(k):
1202 src = os.path.join(global_dir, bn)
1203 replaceTree(src, OutDir)
1205 DoLink(global_dir, OutDir, bn)
1209 bn = os.path.basename(k)
1210 target = os.path.join(OutDir, bn)
1211 if os.path.isdir(target):
1214 posix.remove(target)
1217 DoLink(global_dir, OutDir, "last_update.trace")
1220 def getLastLDAPChangeTime(l):
1221 mods = l.search_s('cn=log',
1222 ldap.SCOPE_ONELEVEL,
1223 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1228 # Sort the list by reqEnd
1229 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1230 # Take the last element in the array
1231 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1235 def getLastBuildTime(gdir):
1239 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1240 cache_last_mod=fd.read().split()
1242 cache_last_mod = cache_last_mod[0]
1247 if e.errno == errno.ENOENT:
1252 return cache_last_mod
1259 parser = optparse.OptionParser()
1260 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1261 help="Output directory.")
1262 parser.add_option("-f", "--force", dest="force", action="store_true",
1263 help="Force generation, even if not update to LDAP has happened.")
1265 (options, args) = parser.parse_args()
1271 l = make_ldap_conn()
1273 if options.generatedir is not None:
1274 generate_dir = os.environ['UD_GENERATEDIR']
1275 elif 'UD_GENERATEDIR' in os.environ:
1276 generate_dir = os.environ['UD_GENERATEDIR']
1278 ldap_last_mod = getLastLDAPChangeTime(l)
1279 cache_last_mod = getLastBuildTime(generate_dir)
1280 need_update = ldap_last_mod > cache_last_mod
1282 if not options.force and not need_update:
1283 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1284 fd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1288 # Fetch all the groups
1290 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1291 ["gid", "gidNumber", "subGroup"])
1293 # Generate the SubGroupMap and GroupIDMap
1295 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1297 if x[1].has_key("gidNumber") == 0:
1299 GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1300 if x[1].has_key("subGroup") != 0:
1301 SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1305 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1306 lock = get_lock( lockf )
1308 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1311 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1312 generate_all(generate_dir, l)
1313 tracefd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1317 if lock is not None:
1320 if __name__ == "__main__":
1326 # vim:set shiftwidth=3: