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 Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
576 # Write out the email address for each user
578 if not key in a: continue
581 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
584 # Oops, something unspeakable happened.
588 if Fdb.close() != None:
589 raise "cdbmake gave an error"
591 # Generate the anon XEarth marker file
592 def GenMarkers(accounts, File):
595 F = open(File + ".tmp", "w")
597 # Write out the position for each user
599 if not ('latitude' in a and 'longitude' in a): continue
601 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
602 Line = Sanitize(Line) + "\n"
607 # Oops, something unspeakable happened.
613 # Generate the debian-private subscription list
614 def GenPrivate(accounts, File):
617 F = open(File + ".tmp", "w")
619 # Write out the position for each user
621 if not a.is_active_user(): continue
622 if not 'privateSub' in a: continue
624 Line = "%s"%(a['privateSub'])
625 Line = Sanitize(Line) + "\n"
630 # Oops, something unspeakable happened.
636 # Generate a list of locked accounts
637 def GenDisabledAccounts(accounts, File):
640 F = open(File + ".tmp", "w")
641 disabled_accounts = []
643 # Fetch all the users
645 if a.pw_active(): continue
646 Line = "%s:%s" % (a['uid'], "Account is locked")
647 disabled_accounts.append(a)
648 F.write(Sanitize(Line) + "\n")
650 # Oops, something unspeakable happened.
655 return disabled_accounts
657 # Generate the list of local addresses that refuse all mail
658 def GenMailDisable(accounts, File):
661 F = open(File + ".tmp", "w")
664 if not 'mailDisableMessage' in a: continue
665 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
666 Line = Sanitize(Line) + "\n"
669 # Oops, something unspeakable happened.
675 # Generate a list of uids that should have boolean affects applied
676 def GenMailBool(accounts, File, key):
679 F = open(File + ".tmp", "w")
682 if not key in a: continue
683 if not a[key] == 'TRUE': continue
684 Line = "%s"%(a['uid'])
685 Line = Sanitize(Line) + "\n"
688 # Oops, something unspeakable happened.
694 # Generate a list of hosts for RBL or whitelist purposes.
695 def GenMailList(accounts, File, key):
698 F = open(File + ".tmp", "w")
700 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
701 else: validregex = re.compile('^[-\w.]+$')
704 if not key in a: continue
706 filtered = filter(lambda z: validregex.match(z), a[key])
707 if len(filtered) == 0: continue
708 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
709 line = a['uid'] + ': ' + ' : '.join(filtered)
710 line = Sanitize(line) + "\n"
713 # Oops, something unspeakable happened.
719 def isRoleAccount(account):
720 return 'debianRoleAccount' in account['objectClass']
722 # Generate the DNS Zone file
723 def GenDNS(accounts, File):
726 F = open(File + ".tmp", "w")
728 # Fetch all the users
731 # Write out the zone file entry for each user
733 if not 'dnsZoneEntry' in a: continue
734 if not a.is_active_user() and not isRoleAccount(a): continue
737 F.write("; %s\n"%(a.email_address()))
738 for z in a["dnsZoneEntry"]:
739 Split = z.lower().split()
740 if Split[1].lower() == 'in':
741 Line = " ".join(Split) + "\n"
744 Host = Split[0] + DNSZone
745 if BSMTPCheck.match(Line) != None:
746 F.write("; Has BSMTP\n")
748 # Write some identification information
749 if not RRs.has_key(Host):
750 if Split[2].lower() in ["a", "aaaa"]:
751 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
752 for y in a["keyFingerPrint"]:
753 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
757 Line = "; Err %s"%(str(Split))
762 F.write("; Errors:\n")
763 for line in str(e).split("\n"):
764 F.write("; %s\n"%(line))
767 # Oops, something unspeakable happened.
773 def ExtractDNSInfo(x):
777 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
780 if x[1].has_key("ipHostNumber"):
781 for I in x[1]["ipHostNumber"]:
782 if IsV6Addr.match(I) != None:
783 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
785 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
789 if 'sshRSAHostKey' in x[1]:
790 for I in x[1]["sshRSAHostKey"]:
792 if Split[0] == 'ssh-rsa':
794 if Split[0] == 'ssh-dss':
796 if Algorithm == None:
798 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
799 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
801 if 'architecture' in x[1]:
802 Arch = GetAttr(x, "architecture")
804 if x[1].has_key("machine"):
805 Mach = " " + GetAttr(x, "machine")
806 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
808 if x[1].has_key("mXRecord"):
809 for I in x[1]["mXRecord"]:
810 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
814 # Generate the DNS records
815 def GenZoneRecords(host_attrs, File):
818 F = open(File + ".tmp", "w")
820 # Fetch all the hosts
822 if x[1].has_key("hostname") == 0:
825 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
828 DNSInfo = ExtractDNSInfo(x)
832 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
835 Line = "\t\t\t%s" % (Line)
839 # this would write sshfp lines for services on machines
840 # but we can't yet, since some are cnames and we'll make
841 # an invalid zonefile
843 # for i in x[1].get("purpose", []):
844 # m = PurposeHostField.match(i)
847 # # we ignore [[*..]] entries
848 # if m.startswith('*'):
850 # if m.startswith('-'):
853 # if not m.endswith(HostDomain):
855 # if not m.endswith('.'):
857 # for Line in DNSInfo:
858 # if isSSHFP.match(Line):
859 # Line = "%s\t%s" % (m, Line)
860 # F.write(Line + "\n")
862 # Oops, something unspeakable happened.
868 # Generate the BSMTP file
869 def GenBSMTP(accounts, File, HomePrefix):
872 F = open(File + ".tmp", "w")
874 # Write out the zone file entry for each user
876 if not 'dnsZoneEntry' in a: continue
877 if not a.is_active_user(): continue
880 for z in a["dnsZoneEntry"]:
881 Split = z.lower().split()
882 if Split[1].lower() == 'in':
883 for y in range(0, len(Split)):
886 Line = " ".join(Split) + "\n"
888 Host = Split[0] + DNSZone
889 if BSMTPCheck.match(Line) != None:
890 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
891 a['uid'], HomePrefix, a['uid'], Host))
894 F.write("; Errors\n")
897 # Oops, something unspeakable happened.
903 def HostToIP(Host, mapped=True):
907 if Host[1].has_key("ipHostNumber"):
908 for addr in Host[1]["ipHostNumber"]:
909 IPAdresses.append(addr)
910 if IsV6Addr.match(addr) is None and mapped == "True":
911 IPAdresses.append("::ffff:"+addr)
915 # Generate the ssh known hosts file
916 def GenSSHKnown(host_attrs, File, mode=None):
919 OldMask = os.umask(0022)
920 F = open(File + ".tmp", "w", 0644)
924 if x[1].has_key("hostname") == 0 or \
925 x[1].has_key("sshRSAHostKey") == 0:
927 Host = GetAttr(x, "hostname")
929 if Host.endswith(HostDomain):
930 HostNames.append(Host[:-(len(HostDomain) + 1)])
932 # in the purpose field [[host|some other text]] (where some other text is optional)
933 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
934 # file. But so that we don't have to add everything we link we can add an asterisk
935 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
936 # http linking it we also support [[-hostname]] entries.
937 for i in x[1].get("purpose", []):
938 m = PurposeHostField.match(i)
941 # we ignore [[*..]] entries
942 if m.startswith('*'):
944 if m.startswith('-'):
948 if m.endswith(HostDomain):
949 HostNames.append(m[:-(len(HostDomain) + 1)])
951 for I in x[1]["sshRSAHostKey"]:
952 if mode and mode == 'authorized_keys':
954 if 'sshdistAuthKeysHost' in x[1]:
955 hosts += x[1]['sshdistAuthKeysHost']
956 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)
958 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
959 Line = Sanitize(Line) + "\n"
961 # Oops, something unspeakable happened.
967 # Generate the debianhosts file (list of all IP addresses)
968 def GenHosts(host_attrs, File):
971 OldMask = os.umask(0022)
972 F = open(File + ".tmp", "w", 0644)
979 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
982 if not 'ipHostNumber' in x[1]:
985 addrs = x[1]["ipHostNumber"]
989 addr = Sanitize(addr) + "\n"
992 # Oops, something unspeakable happened.
998 def replaceTree(src, dst_basedir):
999 bn = os.path.basename(src)
1000 dst = os.path.join(dst_basedir, bn)
1002 shutil.copytree(src, dst)
1004 def GenKeyrings(OutDir):
1006 if os.path.isdir(k):
1007 replaceTree(k, OutDir)
1009 shutil.copy(k, OutDir)
1012 def get_accounts(ldap_conn):
1013 # Fetch all the users
1014 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1015 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1016 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1017 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1018 "shadowExpire", "emailForward", "latitude", "longitude",\
1019 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1020 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1021 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1022 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1023 "mailContentInspectionAction", "webPassword"])
1025 if passwd_attrs is None:
1026 raise UDEmptyList, "No Users"
1027 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1028 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1032 def get_hosts(ldap_conn):
1033 # Fetch all the hosts
1034 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1035 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1036 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1038 if HostAttrs == None:
1039 raise UDEmptyList, "No Hosts"
1041 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1046 def make_ldap_conn():
1047 # Connect to the ldap server
1049 # for testing purposes it's sometimes useful to pass username/password
1050 # via the environment
1051 if 'UD_CREDENTIALS' in os.environ:
1052 Pass = os.environ['UD_CREDENTIALS'].split()
1054 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1055 Pass = F.readline().strip().split(" ")
1057 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1063 def setup_group_maps(l):
1064 # Fetch all the groups
1067 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1068 ["gid", "gidNumber", "subGroup"])
1070 # Generate the subgroup_map and group_id_map
1072 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1074 if x[1].has_key("gidNumber") == 0:
1076 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1077 if x[1].has_key("subGroup") != 0:
1078 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1082 SubGroupMap = subgroup_map
1083 GroupIDMap = group_id_map
1085 def generate_all(global_dir, ldap_conn):
1086 accounts = get_accounts(ldap_conn)
1087 host_attrs = get_hosts(ldap_conn)
1090 # Generate global things
1091 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1093 accounts = filter(lambda x: not IsRetired(x), accounts)
1094 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1096 CheckForward(accounts)
1098 GenMailDisable(accounts, global_dir + "mail-disable")
1099 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1100 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1101 GenPrivate(accounts, global_dir + "debian-private")
1102 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1103 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1104 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1105 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1106 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1107 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1108 GenWebPassword(accounts, global_dir + "web-passwords")
1109 GenKeyrings(global_dir)
1112 GenForward(accounts, global_dir + "forward-alias")
1114 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1115 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1117 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1118 GenMarkers(accounts, global_dir + "markers")
1119 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1120 GenHosts(host_attrs, global_dir + "debianhosts")
1121 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1123 GenDNS(accounts, global_dir + "dns-zone")
1124 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1126 setup_group_maps(ldap_conn)
1128 for host in host_attrs:
1129 if not "hostname" in host[1]:
1131 generate_host(host, global_dir, accounts, ssh_userkeys)
1133 def generate_host(host, global_dir, accounts, ssh_userkeys):
1134 current_host = host[1]['hostname'][0]
1135 OutDir = global_dir + current_host + '/'
1136 if not os.path.isdir(OutDir):
1139 # Get the group list and convert any named groups to numerics
1141 for groupname in AllowedGroupsPreload.strip().split(" "):
1142 GroupList[groupname] = True
1143 if 'allowedGroups' in host[1]:
1144 for groupname in host[1]['allowedGroups']:
1145 GroupList[groupname] = True
1146 for groupname in GroupList.keys():
1147 if groupname in GroupIDMap:
1148 GroupList[str(GroupIDMap[groupname])] = True
1151 if 'exportOptions' in host[1]:
1152 for extra in host[1]['exportOptions']:
1153 ExtraList[extra.upper()] = True
1156 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), accounts)
1158 DoLink(global_dir, OutDir, "debianhosts")
1159 DoLink(global_dir, OutDir, "ssh_known_hosts")
1160 DoLink(global_dir, OutDir, "disabled-accounts")
1163 if 'NOPASSWD' in ExtraList:
1164 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1166 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1168 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1169 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1171 # Now we know who we're allowing on the machine, export
1172 # the relevant ssh keys
1173 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1175 if not 'NOPASSWD' in ExtraList:
1176 GenShadow(accounts, OutDir + "shadow")
1178 # Link in global things
1179 if not 'NOMARKERS' in ExtraList:
1180 DoLink(global_dir, OutDir, "markers")
1181 DoLink(global_dir, OutDir, "mail-forward.cdb")
1182 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1183 DoLink(global_dir, OutDir, "mail-disable")
1184 DoLink(global_dir, OutDir, "mail-greylist")
1185 DoLink(global_dir, OutDir, "mail-callout")
1186 DoLink(global_dir, OutDir, "mail-rbl")
1187 DoLink(global_dir, OutDir, "mail-rhsbl")
1188 DoLink(global_dir, OutDir, "mail-whitelist")
1189 DoLink(global_dir, OutDir, "all-accounts.json")
1190 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1191 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1192 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1195 DoLink(global_dir, OutDir, "forward-alias")
1197 if 'DNS' in ExtraList:
1198 DoLink(global_dir, OutDir, "dns-zone")
1199 DoLink(global_dir, OutDir, "dns-sshfp")
1201 if 'AUTHKEYS' in ExtraList:
1202 DoLink(global_dir, OutDir, "authorized_keys")
1204 if 'BSMTP' in ExtraList:
1205 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1207 if 'PRIVATE' in ExtraList:
1208 DoLink(global_dir, OutDir, "debian-private")
1210 if 'GITOLITE' in ExtraList:
1211 DoLink(global_dir, OutDir, "ssh-gitolite")
1213 if 'WEB-PASSWORDS' in ExtraList:
1214 DoLink(global_dir, OutDir, "web-passwords")
1216 if 'KEYRING' in ExtraList:
1218 bn = os.path.basename(k)
1219 if os.path.isdir(k):
1220 src = os.path.join(global_dir, bn)
1221 replaceTree(src, OutDir)
1223 DoLink(global_dir, OutDir, bn)
1227 bn = os.path.basename(k)
1228 target = os.path.join(OutDir, bn)
1229 if os.path.isdir(target):
1232 posix.remove(target)
1235 DoLink(global_dir, OutDir, "last_update.trace")
1238 def getLastLDAPChangeTime(l):
1239 mods = l.search_s('cn=log',
1240 ldap.SCOPE_ONELEVEL,
1241 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1246 # Sort the list by reqEnd
1247 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1248 # Take the last element in the array
1249 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1253 def getLastBuildTime(gdir):
1257 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1258 cache_last_mod=fd.read().split()
1260 cache_last_mod = cache_last_mod[0]
1265 if e.errno == errno.ENOENT:
1270 return cache_last_mod
1274 parser = optparse.OptionParser()
1275 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1276 help="Output directory.")
1277 parser.add_option("-f", "--force", dest="force", action="store_true",
1278 help="Force generation, even if not update to LDAP has happened.")
1280 (options, args) = parser.parse_args()
1286 l = make_ldap_conn()
1288 if options.generatedir is not None:
1289 generate_dir = os.environ['UD_GENERATEDIR']
1290 elif 'UD_GENERATEDIR' in os.environ:
1291 generate_dir = os.environ['UD_GENERATEDIR']
1293 ldap_last_mod = getLastLDAPChangeTime(l)
1294 cache_last_mod = getLastBuildTime(generate_dir)
1295 need_update = ldap_last_mod > cache_last_mod
1297 if not options.force and not need_update:
1298 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1299 fd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
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: