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, getopt, 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")
63 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
65 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
66 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
67 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
68 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
69 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
70 isSSHFP = re.compile("^\s*IN\s+SSHFP")
71 DNSZone = ".debian.net"
72 Keyrings = ConfModule.sync_keyrings.split(":")
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):
160 # See if the primary group is in the list
161 if str(account['gidNumber']) in Allowed: return True
163 # Check the host based ACL
164 if account.is_allowed_by_hostacl(CurrentHost): return True
166 # See if there are supplementary groups
167 if not 'supplementaryGid' in account: return False
170 addGroups(supgroups, account['supplementaryGid'], account['uid'])
172 if Allowed.has_key(g):
176 def Die(File, F, Fdb):
182 os.remove(File + ".tmp")
186 os.remove(File + ".tdb.tmp")
190 def Done(File, F, Fdb):
193 os.rename(File + ".tmp", File)
196 os.rename(File + ".tdb.tmp", File + ".tdb")
198 # Generate the password list
199 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
202 F = open(File + ".tdb.tmp", "w")
207 if not IsInGroup(a): continue
209 # Do not let people try to buffer overflow some busted passwd parser.
210 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
212 userlist[a['uid']] = a['gidNumber']
213 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
219 HomePrefix, a['uid'],
221 line = Sanitize(line) + "\n"
222 F.write("0%u %s" % (i, line))
223 F.write(".%s %s" % (a['uid'], line))
224 F.write("=%d %s" % (a['uidNumber'], line))
227 # Oops, something unspeakable happened.
233 # Return the list of users so we know which keys to export
236 def GenAllUsers(accounts, file):
239 OldMask = os.umask(0022)
240 f = open(file + ".tmp", "w", 0644)
245 all.append( { 'uid': a['uid'],
246 'uidNumber': a['uidNumber'],
247 'active': a.pw_active() and a.shadow_active() } )
250 # Oops, something unspeakable happened.
256 # Generate the shadow list
257 def GenShadow(accounts, File):
260 OldMask = os.umask(0077)
261 F = open(File + ".tdb.tmp", "w", 0600)
267 if not IsInGroup(a): continue
269 # If the account is locked, mark it as such in shadow
270 # See Debian Bug #308229 for why we set it to 1 instead of 0
271 if not a.pw_active(): ShadowExpire = '1'
272 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
273 else: ShadowExpire = ''
276 values.append(a['uid'])
277 values.append(a.get_password())
278 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
279 if key in a: values.append(a[key])
280 else: values.append('')
281 values.append(ShadowExpire)
282 line = ':'.join(values)+':'
283 line = Sanitize(line) + "\n"
284 F.write("0%u %s" % (i, line))
285 F.write(".%s %s" % (a['uid'], line))
288 # Oops, something unspeakable happened.
294 # Generate the sudo passwd file
295 def GenShadowSudo(accounts, File, untrusted):
298 OldMask = os.umask(0077)
299 F = open(File + ".tmp", "w", 0600)
304 if not IsInGroup(a): continue
306 if 'sudoPassword' in a:
307 for entry in a['sudoPassword']:
308 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
311 uuid = Match.group(1)
312 status = Match.group(2)
313 hosts = Match.group(3)
314 cryptedpass = Match.group(4)
316 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
318 for_all = hosts == "*"
319 for_this_host = CurrentHost in hosts.split(',')
320 if not (for_all or for_this_host):
322 # ignore * passwords for untrusted hosts, but copy host specific passwords
323 if for_all and untrusted:
326 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
331 Line = "%s:%s" % (a['uid'], Pass)
332 Line = Sanitize(Line) + "\n"
333 F.write("%s" % (Line))
335 # Oops, something unspeakable happened.
341 # Generate the shadow list
342 def GenSSHShadow(global_dir, accounts):
343 # Fetch all the users
346 safe_rmtree(os.path.join(global_dir, 'userkeys'))
347 safe_makedirs(os.path.join(global_dir, 'userkeys'))
350 if not 'sshRSAAuthKey' in a: continue
354 OldMask = os.umask(0077)
355 File = os.path.join(global_dir, 'userkeys', a['uid'])
356 F = open(File + ".tmp", "w", 0600)
359 for I in a['sshRSAAuthKey']:
360 MultipleLine = "%s" % I
361 MultipleLine = Sanitize(MultipleLine) + "\n"
362 F.write(MultipleLine)
365 userfiles.append(os.path.basename(File))
367 # Oops, something unspeakable happened.
370 # As neither masterFileName nor masterFile are defined at any point
371 # this will raise a NameError.
372 Die(masterFileName, masterFile, None)
377 # Generate the webPassword list
378 def GenWebPassword(accounts, File):
381 OldMask = os.umask(0077)
382 F = open(File, "w", 0600)
386 if not 'webPassword' in a: continue
387 if not a.pw_active(): continue
389 Pass = str(a['webPassword'])
390 Line = "%s:%s" % (a['uid'], Pass)
391 Line = Sanitize(Line) + "\n"
392 F.write("%s" % (Line))
398 def GenSSHtarballs(global_dir, userlist, SSHFiles, grouprevmap, target):
399 OldMask = os.umask(0077)
400 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
402 for f in userlist.keys():
403 if f not in SSHFiles:
405 # If we're not exporting their primary group, don't export
408 if userlist[f] in grouprevmap.keys():
409 grname = grouprevmap[userlist[f]]
412 if int(userlist[f]) <= 100:
413 # In these cases, look it up in the normal way so we
414 # deal with cases where, for instance, users are in group
415 # users as their primary group.
416 grname = grp.getgrgid(userlist[f])[0]
421 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])
424 to = tf.gettarinfo(os.path.join(global_dir, 'userkeys', f), f)
425 # These will only be used where the username doesn't
426 # exist on the target system for some reason; hence,
427 # in those cases, the safest thing is for the file to
428 # be owned by root but group nobody. This deals with
429 # the bloody obscure case where the group fails to exist
430 # whilst the user does (in which case we want to avoid
431 # ending up with a file which is owned user:root to avoid
432 # a fairly obvious attack vector)
435 # Using the username / groupname fields avoids any need
436 # to give a shit^W^W^Wcare about the UIDoffset stuff.
441 contents = file(os.path.join(global_dir, 'userkeys', f)).read()
443 for line in contents.splitlines():
444 if line.startswith("allowed_hosts=") and ' ' in line:
445 machines, line = line.split('=', 1)[1].split(' ', 1)
446 if CurrentHost not in machines.split(','):
447 continue # skip this key
450 continue # no keys for this host
451 contents = "\n".join(lines) + "\n"
452 to.size = len(contents)
453 tf.addfile(to, StringIO(contents))
456 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
458 # add a list of groups to existing groups,
459 # including all subgroups thereof, recursively.
460 # basically this proceduces the transitive hull of the groups in
462 def addGroups(existingGroups, newGroups, uid):
463 for group in newGroups:
464 # if it's a <group>@host, split it and verify it's on the current host.
465 s = group.split('@', 1)
466 if len(s) == 2 and s[1] != CurrentHost:
470 # let's see if we handled this group already
471 if group in existingGroups:
474 if not GroupIDMap.has_key(group):
475 print "Group", group, "does not exist but", uid, "is in it"
478 existingGroups.append(group)
480 if SubGroupMap.has_key(group):
481 addGroups(existingGroups, SubGroupMap[group], uid)
483 # Generate the group list
484 def GenGroup(accounts, File):
488 F = open(File + ".tdb.tmp", "w")
490 # Generate the GroupMap
492 for x in GroupIDMap.keys():
494 GroupHasPrimaryMembers = {}
496 # Sort them into a list of groups having a set of users
498 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
499 if not IsInGroup(a): continue
500 if not 'supplementaryGid' in a: continue
503 addGroups(supgroups, a['supplementaryGid'], a['uid'])
505 GroupMap[g].append(a['uid'])
507 # Output the group file.
509 for x in GroupMap.keys():
510 if GroupIDMap.has_key(x) == 0:
513 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
516 grouprevmap[GroupIDMap[x]] = x
518 Line = "%s:x:%u:" % (x, GroupIDMap[x])
520 for I in GroupMap[x]:
521 Line = Line + ("%s%s" % (Comma, I))
523 Line = Sanitize(Line) + "\n"
524 F.write("0%u %s" % (J, Line))
525 F.write(".%s %s" % (x, Line))
526 F.write("=%u %s" % (GroupIDMap[x], Line))
529 # Oops, something unspeakable happened.
537 def CheckForward(accounts):
539 if not 'emailForward' in a: continue
544 if not IsInGroup(a): delete = True
545 # Do not allow people to try to buffer overflow busted parsers
546 elif 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 for y in range(0, len(Split)):
748 Line = " ".join(Split) + "\n"
751 Host = Split[0] + DNSZone
752 if BSMTPCheck.match(Line) != None:
753 F.write("; Has BSMTP\n")
755 # Write some identification information
756 if not RRs.has_key(Host):
757 if Split[2].lower() in ["a", "aaaa"]:
758 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
759 for y in a["keyFingerPrint"]:
760 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
764 Line = "; Err %s"%(str(Split))
769 F.write("; Errors:\n")
770 for line in str(e).split("\n"):
771 F.write("; %s\n"%(line))
774 # Oops, something unspeakable happened.
780 def ExtractDNSInfo(x):
784 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
787 if x[1].has_key("ipHostNumber"):
788 for I in x[1]["ipHostNumber"]:
789 if IsV6Addr.match(I) != None:
790 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
792 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
796 if 'sshRSAHostKey' in x[1]:
797 for I in x[1]["sshRSAHostKey"]:
799 if Split[0] == 'ssh-rsa':
801 if Split[0] == 'ssh-dss':
803 if Algorithm == None:
805 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
806 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
808 if 'architecture' in x[1]:
809 Arch = GetAttr(x, "architecture")
811 if x[1].has_key("machine"):
812 Mach = " " + GetAttr(x, "machine")
813 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
815 if x[1].has_key("mXRecord"):
816 for I in x[1]["mXRecord"]:
817 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
821 # Generate the DNS records
822 def GenZoneRecords(host_attrs, File):
825 F = open(File + ".tmp", "w")
827 # Fetch all the hosts
829 if x[1].has_key("hostname") == 0:
832 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
835 DNSInfo = ExtractDNSInfo(x)
839 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
842 Line = "\t\t\t%s" % (Line)
846 # this would write sshfp lines for services on machines
847 # but we can't yet, since some are cnames and we'll make
848 # an invalid zonefile
850 # for i in x[1].get("purpose", []):
851 # m = PurposeHostField.match(i)
854 # # we ignore [[*..]] entries
855 # if m.startswith('*'):
857 # if m.startswith('-'):
860 # if not m.endswith(HostDomain):
862 # if not m.endswith('.'):
864 # for Line in DNSInfo:
865 # if isSSHFP.match(Line):
866 # Line = "%s\t%s" % (m, Line)
867 # F.write(Line + "\n")
869 # Oops, something unspeakable happened.
875 # Generate the BSMTP file
876 def GenBSMTP(accounts, File, HomePrefix):
879 F = open(File + ".tmp", "w")
881 # Write out the zone file entry for each user
883 if not 'dnsZoneEntry' in a: continue
884 if not a.is_active_user(): continue
887 for z in a["dnsZoneEntry"]:
888 Split = z.lower().split()
889 if Split[1].lower() == 'in':
890 for y in range(0, len(Split)):
893 Line = " ".join(Split) + "\n"
895 Host = Split[0] + DNSZone
896 if BSMTPCheck.match(Line) != None:
897 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
898 a['uid'], HomePrefix, a['uid'], Host))
901 F.write("; Errors\n")
904 # Oops, something unspeakable happened.
910 def HostToIP(Host, mapped=True):
914 if Host[1].has_key("ipHostNumber"):
915 for addr in Host[1]["ipHostNumber"]:
916 IPAdresses.append(addr)
917 if IsV6Addr.match(addr) is None and mapped == "True":
918 IPAdresses.append("::ffff:"+addr)
922 # Generate the ssh known hosts file
923 def GenSSHKnown(host_attrs, File, mode=None):
926 OldMask = os.umask(0022)
927 F = open(File + ".tmp", "w", 0644)
931 if x[1].has_key("hostname") == 0 or \
932 x[1].has_key("sshRSAHostKey") == 0:
934 Host = GetAttr(x, "hostname")
936 if Host.endswith(HostDomain):
937 HostNames.append(Host[:-(len(HostDomain) + 1)])
939 # in the purpose field [[host|some other text]] (where some other text is optional)
940 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
941 # file. But so that we don't have to add everything we link we can add an asterisk
942 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
943 # http linking it we also support [[-hostname]] entries.
944 for i in x[1].get("purpose", []):
945 m = PurposeHostField.match(i)
948 # we ignore [[*..]] entries
949 if m.startswith('*'):
951 if m.startswith('-'):
955 if m.endswith(HostDomain):
956 HostNames.append(m[:-(len(HostDomain) + 1)])
958 for I in x[1]["sshRSAHostKey"]:
959 if mode and mode == 'authorized_keys':
961 if 'sshdistAuthKeysHost' in x[1]:
962 hosts += x[1]['sshdistAuthKeysHost']
963 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)
965 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
966 Line = Sanitize(Line) + "\n"
968 # Oops, something unspeakable happened.
974 # Generate the debianhosts file (list of all IP addresses)
975 def GenHosts(host_attrs, File):
978 OldMask = os.umask(0022)
979 F = open(File + ".tmp", "w", 0644)
986 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
989 if not 'ipHostNumber' in x[1]:
992 addrs = x[1]["ipHostNumber"]
996 addr = Sanitize(addr) + "\n"
999 # Oops, something unspeakable happened.
1005 def replaceTree(src, dst_basedir):
1006 bn = os.path.basename(src)
1007 dst = os.path.join(dst_basedir, bn)
1009 shutil.copytree(src, dst)
1011 def GenKeyrings(OutDir):
1013 if os.path.isdir(k):
1014 replaceTree(k, OutDir)
1016 shutil.copy(k, OutDir)
1019 def get_accounts(ldap_conn):
1020 # Fetch all the users
1021 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1022 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1023 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1024 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1025 "shadowExpire", "emailForward", "latitude", "longitude",\
1026 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1027 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1028 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1029 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1030 "mailContentInspectionAction", "webPassword"])
1032 if passwd_attrs is None:
1033 raise UDEmptyList, "No Users"
1034 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1035 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1039 def get_hosts(ldap_conn):
1040 # Fetch all the hosts
1041 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1042 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1043 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1045 if HostAttrs == None:
1046 raise UDEmptyList, "No Hosts"
1048 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1053 def make_ldap_conn():
1054 # Connect to the ldap server
1056 # for testing purposes it's sometimes useful to pass username/password
1057 # via the environment
1058 if 'UD_CREDENTIALS' in os.environ:
1059 Pass = os.environ['UD_CREDENTIALS'].split()
1061 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1062 Pass = F.readline().strip().split(" ")
1064 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1068 def generate_all(global_dir, ldap_conn):
1069 accounts = get_accounts(ldap_conn)
1070 host_attrs = get_hosts(ldap_conn)
1073 # Generate global things
1074 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1076 accounts = filter(lambda x: not IsRetired(x), accounts)
1077 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1079 CheckForward(accounts)
1081 GenMailDisable(accounts, global_dir + "mail-disable")
1082 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1083 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1084 GenPrivate(accounts, global_dir + "debian-private")
1085 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1086 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1087 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1088 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1089 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1090 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1091 GenWebPassword(accounts, global_dir + "web-passwords")
1092 GenKeyrings(global_dir)
1095 GenForward(accounts, global_dir + "forward-alias")
1097 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1098 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1100 ssh_files = GenSSHShadow(global_dir, accounts)
1101 GenMarkers(accounts, global_dir + "markers")
1102 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1103 GenHosts(host_attrs, global_dir + "debianhosts")
1105 GenDNS(accounts, global_dir + "dns-zone")
1106 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1108 for host in host_attrs:
1109 if not "hostname" in host[1]:
1111 generate_host(host, global_dir, accounts, ssh_files)
1113 def generate_host(host, global_dir, accounts, ssh_files):
1116 CurrentHost = host[1]['hostname'][0]
1117 OutDir = global_dir + CurrentHost + '/'
1123 # Get the group list and convert any named groups to numerics
1125 for groupname in AllowedGroupsPreload.strip().split(" "):
1126 GroupList[groupname] = True
1127 if 'allowedGroups' in host[1]:
1128 for groupname in host[1]['allowedGroups']:
1129 GroupList[groupname] = True
1130 for groupname in GroupList.keys():
1131 if groupname in GroupIDMap:
1132 GroupList[str(GroupIDMap[groupname])] = True
1135 if 'exportOptions' in host[1]:
1136 for extra in host[1]['exportOptions']:
1137 ExtraList[extra.upper()] = True
1144 DoLink(global_dir, OutDir, "debianhosts")
1145 DoLink(global_dir, OutDir, "ssh_known_hosts")
1146 DoLink(global_dir, OutDir, "disabled-accounts")
1149 if 'NOPASSWD' in ExtraList:
1150 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1152 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1154 grouprevmap = GenGroup(accounts, OutDir + "group")
1155 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1157 # Now we know who we're allowing on the machine, export
1158 # the relevant ssh keys
1159 GenSSHtarballs(global_dir, userlist, ssh_files, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1161 if not 'NOPASSWD' in ExtraList:
1162 GenShadow(accounts, OutDir + "shadow")
1164 # Link in global things
1165 if not 'NOMARKERS' in ExtraList:
1166 DoLink(global_dir, OutDir, "markers")
1167 DoLink(global_dir, OutDir, "mail-forward.cdb")
1168 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1169 DoLink(global_dir, OutDir, "mail-disable")
1170 DoLink(global_dir, OutDir, "mail-greylist")
1171 DoLink(global_dir, OutDir, "mail-callout")
1172 DoLink(global_dir, OutDir, "mail-rbl")
1173 DoLink(global_dir, OutDir, "mail-rhsbl")
1174 DoLink(global_dir, OutDir, "mail-whitelist")
1175 DoLink(global_dir, OutDir, "all-accounts.json")
1176 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "user-forward.cdb", 'emailForward')
1177 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "batv-tokens.cdb", 'bATVToken')
1178 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1181 DoLink(global_dir, OutDir, "forward-alias")
1183 if 'DNS' in ExtraList:
1184 DoLink(global_dir, OutDir, "dns-zone")
1185 DoLink(global_dir, OutDir, "dns-sshfp")
1187 if 'AUTHKEYS' in ExtraList:
1188 DoLink(global_dir, OutDir, "authorized_keys")
1190 if 'BSMTP' in ExtraList:
1191 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1193 if 'PRIVATE' in ExtraList:
1194 DoLink(global_dir, OutDir, "debian-private")
1196 if 'WEB-PASSWORDS' in ExtraList:
1197 DoLink(global_dir, OutDir, "web-passwords")
1199 if 'KEYRING' in ExtraList:
1201 bn = os.path.basename(k)
1202 if os.path.isdir(k):
1203 src = os.path.join(global_dir, bn)
1204 replaceTree(src, OutDir)
1206 DoLink(global_dir, OutDir, bn)
1210 bn = os.path.basename(k)
1211 target = os.path.join(OutDir, bn)
1212 if os.path.isdir(target):
1215 posix.remove(target)
1218 DoLink(global_dir, OutDir, "last_update.trace")
1220 l = make_ldap_conn()
1222 mods = l.search_s('cn=log',
1223 ldap.SCOPE_ONELEVEL,
1224 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1229 # Sort the list by reqEnd
1230 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1231 # Take the last element in the array
1232 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1234 # override globaldir for testing
1235 if 'UD_GENERATEDIR' in os.environ:
1236 GenerateDir = os.environ['UD_GENERATEDIR']
1238 cache_last_mod = [0,0]
1241 fd = open(os.path.join(GenerateDir, "last_update.trace"), "r")
1242 cache_last_mod=fd.read().split()
1245 if e.errno == errno.ENOENT:
1250 if cache_last_mod[0] >= last:
1251 fd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
1252 fd.write("%s\n%s\n" % (last, int(time.time())))
1256 # Fetch all the groups
1258 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1259 ["gid", "gidNumber", "subGroup"])
1261 # Generate the SubGroupMap and GroupIDMap
1263 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1265 if x[1].has_key("gidNumber") == 0:
1267 GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1268 if x[1].has_key("subGroup") != 0:
1269 SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1273 lockf = os.path.join(GenerateDir, 'ud-generate.lock')
1274 lock = get_lock( lockf )
1276 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1279 generate_all(GenerateDir, l)
1282 if lock is not None:
1285 fd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
1286 fd.write("%s\n%s\n" % (last, int(time.time())))
1291 # vim:set shiftwidth=3: