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, fcntl
32 from userdir_ldap import *
33 from userdir_exceptions import *
36 from cStringIO import StringIO
38 from StringIO import StringIO
40 import simplejson as json
43 if not '__author__' in json.__dict__:
44 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
45 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
48 sys.stderr.write("You should probably not run ud-generate as root.\n")
60 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
62 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
63 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
64 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
65 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
66 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
67 isSSHFP = re.compile("^\s*IN\s+SSHFP")
68 DNSZone = ".debian.net"
69 Keyrings = ConfModule.sync_keyrings.split(":")
70 GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
73 def safe_makedirs(dir):
77 if e.errno == errno.EEXIST:
86 if e.errno == errno.ENOENT:
91 def get_lock(fn, wait=5*60):
94 ends = time.time() + wait
99 fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
103 if time.time() >= ends:
105 sl = min(sl*2, 10, ends - time.time())
111 return Str.translate(string.maketrans("\n\r\t", "$$$"))
113 def DoLink(From, To, File):
115 posix.remove(To + File)
118 posix.link(From + File, To + File)
120 def IsRetired(account):
122 Looks for accountStatus in the LDAP record and tries to
123 match it against one of the known retired statuses
126 status = account['accountStatus']
128 line = status.split()
131 if status == "inactive":
134 elif status == "memorial":
137 elif status == "retiring":
138 # We'll give them a few extra days over what we said
139 age = 6 * 31 * 24 * 60 * 60
141 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
149 #def IsGidDebian(account):
150 # return account['gidNumber'] == 800
152 # See if this user is in the group list
153 def IsInGroup(account, allowed, current_host):
154 # See if the primary group is in the list
155 if str(account['gidNumber']) in allowed: return True
157 # Check the host based ACL
158 if account.is_allowed_by_hostacl(current_host): return True
160 # See if there are supplementary groups
161 if not 'supplementaryGid' in account: return False
164 addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
166 if allowed.has_key(g):
170 def Die(File, F, Fdb):
176 os.remove(File + ".tmp")
180 os.remove(File + ".tdb.tmp")
184 def Done(File, F, Fdb):
187 os.rename(File + ".tmp", File)
190 os.rename(File + ".tdb.tmp", File + ".tdb")
192 # Generate the password list
193 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
196 F = open(File + ".tdb.tmp", "w")
201 # Do not let people try to buffer overflow some busted passwd parser.
202 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
204 userlist[a['uid']] = a['gidNumber']
205 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
211 HomePrefix, a['uid'],
213 line = Sanitize(line) + "\n"
214 F.write("0%u %s" % (i, line))
215 F.write(".%s %s" % (a['uid'], line))
216 F.write("=%d %s" % (a['uidNumber'], line))
219 # Oops, something unspeakable happened.
225 # Return the list of users so we know which keys to export
228 def GenAllUsers(accounts, file):
231 OldMask = os.umask(0022)
232 f = open(file + ".tmp", "w", 0644)
237 all.append( { 'uid': a['uid'],
238 'uidNumber': a['uidNumber'],
239 'active': a.pw_active() and a.shadow_active() } )
242 # Oops, something unspeakable happened.
248 # Generate the shadow list
249 def GenShadow(accounts, File):
252 OldMask = os.umask(0077)
253 F = open(File + ".tdb.tmp", "w", 0600)
258 # If the account is locked, mark it as such in shadow
259 # See Debian Bug #308229 for why we set it to 1 instead of 0
260 if not a.pw_active(): ShadowExpire = '1'
261 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
262 else: ShadowExpire = ''
265 values.append(a['uid'])
266 values.append(a.get_password())
267 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
268 if key in a: values.append(a[key])
269 else: values.append('')
270 values.append(ShadowExpire)
271 line = ':'.join(values)+':'
272 line = Sanitize(line) + "\n"
273 F.write("0%u %s" % (i, line))
274 F.write(".%s %s" % (a['uid'], line))
277 # Oops, something unspeakable happened.
283 # Generate the sudo passwd file
284 def GenShadowSudo(accounts, File, untrusted, current_host):
287 OldMask = os.umask(0077)
288 F = open(File + ".tmp", "w", 0600)
293 if 'sudoPassword' in a:
294 for entry in a['sudoPassword']:
295 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
298 uuid = Match.group(1)
299 status = Match.group(2)
300 hosts = Match.group(3)
301 cryptedpass = Match.group(4)
303 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
305 for_all = hosts == "*"
306 for_this_host = current_host in hosts.split(',')
307 if not (for_all or for_this_host):
309 # ignore * passwords for untrusted hosts, but copy host specific passwords
310 if for_all and untrusted:
313 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
318 Line = "%s:%s" % (a['uid'], Pass)
319 Line = Sanitize(Line) + "\n"
320 F.write("%s" % (Line))
322 # Oops, something unspeakable happened.
328 # Generate the sudo passwd file
329 def GenSSHGitolite(accounts, File):
332 OldMask = os.umask(0022)
333 F = open(File + ".tmp", "w", 0600)
336 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
338 if not 'sshRSAAuthKey' in a: continue
341 prefix = GitoliteSSHRestrictions.replace('@@USER@@', User)
342 for I in a["sshRSAAuthKey"]:
343 if I.startswith('ssh-'):
344 line = "%s %s"%(prefix, I)
346 line = "%s,%s"%(prefix, I)
347 line = Sanitize(line) + "\n"
350 # Oops, something unspeakable happened.
356 # Generate the shadow list
357 def GenSSHShadow(global_dir, accounts):
358 # Fetch all the users
362 if not 'sshRSAAuthKey' in a: continue
365 for I in a['sshRSAAuthKey']:
366 MultipleLine = "%s" % I
367 MultipleLine = Sanitize(MultipleLine)
368 contents.append(MultipleLine)
369 userkeys[a['uid']] = contents
372 # Generate the webPassword list
373 def GenWebPassword(accounts, File):
376 OldMask = os.umask(0077)
377 F = open(File, "w", 0600)
381 if not 'webPassword' in a: continue
382 if not a.pw_active(): continue
384 Pass = str(a['webPassword'])
385 Line = "%s:%s" % (a['uid'], Pass)
386 Line = Sanitize(Line) + "\n"
387 F.write("%s" % (Line))
393 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
394 OldMask = os.umask(0077)
395 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
398 if f not in ssh_userkeys:
400 # If we're not exporting their primary group, don't export
403 if userlist[f] in grouprevmap.keys():
404 grname = grouprevmap[userlist[f]]
407 if int(userlist[f]) <= 100:
408 # In these cases, look it up in the normal way so we
409 # deal with cases where, for instance, users are in group
410 # users as their primary group.
411 grname = grp.getgrgid(userlist[f])[0]
416 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])
420 for line in ssh_userkeys[f]:
421 if line.startswith("allowed_hosts=") and ' ' in line:
422 machines, line = line.split('=', 1)[1].split(' ', 1)
423 if current_host not in machines.split(','):
424 continue # skip this key
427 continue # no keys for this host
428 contents = "\n".join(lines) + "\n"
430 to = tarfile.TarInfo(name=f)
431 # These will only be used where the username doesn't
432 # exist on the target system for some reason; hence,
433 # in those cases, the safest thing is for the file to
434 # be owned by root but group nobody. This deals with
435 # the bloody obscure case where the group fails to exist
436 # whilst the user does (in which case we want to avoid
437 # ending up with a file which is owned user:root to avoid
438 # a fairly obvious attack vector)
441 # Using the username / groupname fields avoids any need
442 # to give a shit^W^W^Wcare about the UIDoffset stuff.
446 to.mtime = int(time.time())
447 to.size = len(contents)
449 tf.addfile(to, StringIO(contents))
452 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
454 # add a list of groups to existing groups,
455 # including all subgroups thereof, recursively.
456 # basically this proceduces the transitive hull of the groups in
458 def addGroups(existingGroups, newGroups, uid, current_host):
459 for group in newGroups:
460 # if it's a <group>@host, split it and verify it's on the current host.
461 s = group.split('@', 1)
462 if len(s) == 2 and s[1] != current_host:
466 # let's see if we handled this group already
467 if group in existingGroups:
470 if not GroupIDMap.has_key(group):
471 print "Group", group, "does not exist but", uid, "is in it"
474 existingGroups.append(group)
476 if SubGroupMap.has_key(group):
477 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
479 # Generate the group list
480 def GenGroup(accounts, File, current_host):
484 F = open(File + ".tdb.tmp", "w")
486 # Generate the GroupMap
490 GroupHasPrimaryMembers = {}
492 # Sort them into a list of groups having a set of users
494 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
495 if not 'supplementaryGid' in a: continue
498 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
500 GroupMap[g].append(a['uid'])
502 # Output the group file.
504 for x in GroupMap.keys():
505 if not x in GroupIDMap:
508 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
511 grouprevmap[GroupIDMap[x]] = x
513 Line = "%s:x:%u:" % (x, GroupIDMap[x])
515 for I in GroupMap[x]:
516 Line = Line + ("%s%s" % (Comma, I))
518 Line = Sanitize(Line) + "\n"
519 F.write("0%u %s" % (J, Line))
520 F.write(".%s %s" % (x, Line))
521 F.write("=%u %s" % (GroupIDMap[x], Line))
524 # Oops, something unspeakable happened.
532 def CheckForward(accounts):
534 if not 'emailForward' in a: continue
538 # Do not allow people to try to buffer overflow busted parsers
539 if len(a['emailForward']) > 200: delete = True
540 # Check the forwarding address
541 elif EmailCheck.match(a['emailForward']) is None: delete = True
544 a.delete_mailforward()
546 # Generate the email forwarding list
547 def GenForward(accounts, File):
550 OldMask = os.umask(0022)
551 F = open(File + ".tmp", "w", 0644)
555 if not 'emailForward' in a: continue
556 Line = "%s: %s" % (a['uid'], a['emailForward'])
557 Line = Sanitize(Line) + "\n"
560 # Oops, something unspeakable happened.
566 def GenCDB(accounts, File, key):
569 OldMask = os.umask(0022)
570 # nothing else does the fsync stuff, so why do it here?
571 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
572 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
575 # Write out the email address for each user
577 if not key in a: continue
580 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
583 # Oops, something unspeakable happened.
587 if Fdb.close() != None:
588 raise "cdbmake gave an error"
590 # Generate the anon XEarth marker file
591 def GenMarkers(accounts, File):
594 F = open(File + ".tmp", "w")
596 # Write out the position for each user
598 if not ('latitude' in a and 'longitude' in a): continue
600 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
601 Line = Sanitize(Line) + "\n"
606 # Oops, something unspeakable happened.
612 # Generate the debian-private subscription list
613 def GenPrivate(accounts, File):
616 F = open(File + ".tmp", "w")
618 # Write out the position for each user
620 if not a.is_active_user(): continue
621 if not 'privateSub' in a: continue
623 Line = "%s"%(a['privateSub'])
624 Line = Sanitize(Line) + "\n"
629 # Oops, something unspeakable happened.
635 # Generate a list of locked accounts
636 def GenDisabledAccounts(accounts, File):
639 F = open(File + ".tmp", "w")
640 disabled_accounts = []
642 # Fetch all the users
644 if a.pw_active(): continue
645 Line = "%s:%s" % (a['uid'], "Account is locked")
646 disabled_accounts.append(a)
647 F.write(Sanitize(Line) + "\n")
649 # Oops, something unspeakable happened.
654 return disabled_accounts
656 # Generate the list of local addresses that refuse all mail
657 def GenMailDisable(accounts, File):
660 F = open(File + ".tmp", "w")
663 if not 'mailDisableMessage' in a: continue
664 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
665 Line = Sanitize(Line) + "\n"
668 # Oops, something unspeakable happened.
674 # Generate a list of uids that should have boolean affects applied
675 def GenMailBool(accounts, File, key):
678 F = open(File + ".tmp", "w")
681 if not key in a: continue
682 if not a[key] == 'TRUE': continue
683 Line = "%s"%(a['uid'])
684 Line = Sanitize(Line) + "\n"
687 # Oops, something unspeakable happened.
693 # Generate a list of hosts for RBL or whitelist purposes.
694 def GenMailList(accounts, File, key):
697 F = open(File + ".tmp", "w")
699 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
700 else: validregex = re.compile('^[-\w.]+$')
703 if not key in a: continue
705 filtered = filter(lambda z: validregex.match(z), a[key])
706 if len(filtered) == 0: continue
707 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
708 line = a['uid'] + ': ' + ' : '.join(filtered)
709 line = Sanitize(line) + "\n"
712 # Oops, something unspeakable happened.
718 def isRoleAccount(account):
719 return 'debianRoleAccount' in account['objectClass']
721 # Generate the DNS Zone file
722 def GenDNS(accounts, File):
725 F = open(File + ".tmp", "w")
727 # Fetch all the users
730 # Write out the zone file entry for each user
732 if not 'dnsZoneEntry' in a: continue
733 if not a.is_active_user() and not isRoleAccount(a): continue
736 F.write("; %s\n"%(a.email_address()))
737 for z in a["dnsZoneEntry"]:
738 Split = z.lower().split()
739 if Split[1].lower() == 'in':
740 Line = " ".join(Split) + "\n"
743 Host = Split[0] + DNSZone
744 if BSMTPCheck.match(Line) != None:
745 F.write("; Has BSMTP\n")
747 # Write some identification information
748 if not RRs.has_key(Host):
749 if Split[2].lower() in ["a", "aaaa"]:
750 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
751 for y in a["keyFingerPrint"]:
752 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
756 Line = "; Err %s"%(str(Split))
761 F.write("; Errors:\n")
762 for line in str(e).split("\n"):
763 F.write("; %s\n"%(line))
766 # Oops, something unspeakable happened.
772 def ExtractDNSInfo(x):
776 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
779 if x[1].has_key("ipHostNumber"):
780 for I in x[1]["ipHostNumber"]:
781 if IsV6Addr.match(I) != None:
782 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
784 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
788 if 'sshRSAHostKey' in x[1]:
789 for I in x[1]["sshRSAHostKey"]:
791 if Split[0] == 'ssh-rsa':
793 if Split[0] == 'ssh-dss':
795 if Algorithm == None:
797 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
798 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
800 if 'architecture' in x[1]:
801 Arch = GetAttr(x, "architecture")
803 if x[1].has_key("machine"):
804 Mach = " " + GetAttr(x, "machine")
805 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
807 if x[1].has_key("mXRecord"):
808 for I in x[1]["mXRecord"]:
809 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
813 # Generate the DNS records
814 def GenZoneRecords(host_attrs, File):
817 F = open(File + ".tmp", "w")
819 # Fetch all the hosts
821 if x[1].has_key("hostname") == 0:
824 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
827 DNSInfo = ExtractDNSInfo(x)
831 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
834 Line = "\t\t\t%s" % (Line)
838 # this would write sshfp lines for services on machines
839 # but we can't yet, since some are cnames and we'll make
840 # an invalid zonefile
842 # for i in x[1].get("purpose", []):
843 # m = PurposeHostField.match(i)
846 # # we ignore [[*..]] entries
847 # if m.startswith('*'):
849 # if m.startswith('-'):
852 # if not m.endswith(HostDomain):
854 # if not m.endswith('.'):
856 # for Line in DNSInfo:
857 # if isSSHFP.match(Line):
858 # Line = "%s\t%s" % (m, Line)
859 # F.write(Line + "\n")
861 # Oops, something unspeakable happened.
867 # Generate the BSMTP file
868 def GenBSMTP(accounts, File, HomePrefix):
871 F = open(File + ".tmp", "w")
873 # Write out the zone file entry for each user
875 if not 'dnsZoneEntry' in a: continue
876 if not a.is_active_user(): continue
879 for z in a["dnsZoneEntry"]:
880 Split = z.lower().split()
881 if Split[1].lower() == 'in':
882 for y in range(0, len(Split)):
885 Line = " ".join(Split) + "\n"
887 Host = Split[0] + DNSZone
888 if BSMTPCheck.match(Line) != None:
889 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
890 a['uid'], HomePrefix, a['uid'], Host))
893 F.write("; Errors\n")
896 # Oops, something unspeakable happened.
902 def HostToIP(Host, mapped=True):
906 if Host[1].has_key("ipHostNumber"):
907 for addr in Host[1]["ipHostNumber"]:
908 IPAdresses.append(addr)
909 if IsV6Addr.match(addr) is None and mapped == "True":
910 IPAdresses.append("::ffff:"+addr)
914 # Generate the ssh known hosts file
915 def GenSSHKnown(host_attrs, File, mode=None):
918 OldMask = os.umask(0022)
919 F = open(File + ".tmp", "w", 0644)
923 if x[1].has_key("hostname") == 0 or \
924 x[1].has_key("sshRSAHostKey") == 0:
926 Host = GetAttr(x, "hostname")
928 if Host.endswith(HostDomain):
929 HostNames.append(Host[:-(len(HostDomain) + 1)])
931 # in the purpose field [[host|some other text]] (where some other text is optional)
932 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
933 # file. But so that we don't have to add everything we link we can add an asterisk
934 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
935 # http linking it we also support [[-hostname]] entries.
936 for i in x[1].get("purpose", []):
937 m = PurposeHostField.match(i)
940 # we ignore [[*..]] entries
941 if m.startswith('*'):
943 if m.startswith('-'):
947 if m.endswith(HostDomain):
948 HostNames.append(m[:-(len(HostDomain) + 1)])
950 for I in x[1]["sshRSAHostKey"]:
951 if mode and mode == 'authorized_keys':
953 if 'sshdistAuthKeysHost' in x[1]:
954 hosts += x[1]['sshdistAuthKeysHost']
955 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)
957 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
958 Line = Sanitize(Line) + "\n"
960 # Oops, something unspeakable happened.
966 # Generate the debianhosts file (list of all IP addresses)
967 def GenHosts(host_attrs, File):
970 OldMask = os.umask(0022)
971 F = open(File + ".tmp", "w", 0644)
978 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
981 if not 'ipHostNumber' in x[1]:
984 addrs = x[1]["ipHostNumber"]
988 addr = Sanitize(addr) + "\n"
991 # Oops, something unspeakable happened.
997 def replaceTree(src, dst_basedir):
998 bn = os.path.basename(src)
999 dst = os.path.join(dst_basedir, bn)
1001 shutil.copytree(src, dst)
1003 def GenKeyrings(OutDir):
1005 if os.path.isdir(k):
1006 replaceTree(k, OutDir)
1008 shutil.copy(k, OutDir)
1011 def get_accounts(ldap_conn):
1012 # Fetch all the users
1013 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1014 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1015 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1016 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1017 "shadowExpire", "emailForward", "latitude", "longitude",\
1018 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1019 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1020 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1021 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1022 "mailContentInspectionAction", "webPassword"])
1024 if passwd_attrs is None:
1025 raise UDEmptyList, "No Users"
1026 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1027 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1031 def get_hosts(ldap_conn):
1032 # Fetch all the hosts
1033 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1034 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1035 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1037 if HostAttrs == None:
1038 raise UDEmptyList, "No Hosts"
1040 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1045 def make_ldap_conn():
1046 # Connect to the ldap server
1048 # for testing purposes it's sometimes useful to pass username/password
1049 # via the environment
1050 if 'UD_CREDENTIALS' in os.environ:
1051 Pass = os.environ['UD_CREDENTIALS'].split()
1053 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1054 Pass = F.readline().strip().split(" ")
1056 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1062 def setup_group_maps(l):
1063 # Fetch all the groups
1066 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1067 ["gid", "gidNumber", "subGroup"])
1069 # Generate the subgroup_map and group_id_map
1071 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1073 if x[1].has_key("gidNumber") == 0:
1075 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1076 if x[1].has_key("subGroup") != 0:
1077 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1081 SubGroupMap = subgroup_map
1082 GroupIDMap = group_id_map
1084 def generate_all(global_dir, ldap_conn):
1085 accounts = get_accounts(ldap_conn)
1086 host_attrs = get_hosts(ldap_conn)
1089 # Generate global things
1090 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1092 accounts = filter(lambda x: not IsRetired(x), accounts)
1093 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1095 CheckForward(accounts)
1097 GenMailDisable(accounts, global_dir + "mail-disable")
1098 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1099 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1100 GenPrivate(accounts, global_dir + "debian-private")
1101 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1102 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1103 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1104 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1105 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1106 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1107 GenWebPassword(accounts, global_dir + "web-passwords")
1108 GenKeyrings(global_dir)
1111 GenForward(accounts, global_dir + "forward-alias")
1113 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1114 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1116 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1117 GenMarkers(accounts, global_dir + "markers")
1118 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1119 GenHosts(host_attrs, global_dir + "debianhosts")
1120 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1122 GenDNS(accounts, global_dir + "dns-zone")
1123 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1125 setup_group_maps(ldap_conn)
1127 for host in host_attrs:
1128 if not "hostname" in host[1]:
1130 generate_host(host, global_dir, accounts, ssh_userkeys)
1132 def generate_host(host, global_dir, accounts, ssh_userkeys):
1133 current_host = host[1]['hostname'][0]
1134 OutDir = global_dir + current_host + '/'
1135 if not os.path.isdir(OutDir):
1138 # Get the group list and convert any named groups to numerics
1140 for groupname in AllowedGroupsPreload.strip().split(" "):
1141 GroupList[groupname] = True
1142 if 'allowedGroups' in host[1]:
1143 for groupname in host[1]['allowedGroups']:
1144 GroupList[groupname] = True
1145 for groupname in GroupList.keys():
1146 if groupname in GroupIDMap:
1147 GroupList[str(GroupIDMap[groupname])] = True
1150 if 'exportOptions' in host[1]:
1151 for extra in host[1]['exportOptions']:
1152 ExtraList[extra.upper()] = True
1155 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), accounts)
1157 DoLink(global_dir, OutDir, "debianhosts")
1158 DoLink(global_dir, OutDir, "ssh_known_hosts")
1159 DoLink(global_dir, OutDir, "disabled-accounts")
1162 if 'NOPASSWD' in ExtraList:
1163 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1165 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1167 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1168 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1170 # Now we know who we're allowing on the machine, export
1171 # the relevant ssh keys
1172 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1174 if not 'NOPASSWD' in ExtraList:
1175 GenShadow(accounts, OutDir + "shadow")
1177 # Link in global things
1178 if not 'NOMARKERS' in ExtraList:
1179 DoLink(global_dir, OutDir, "markers")
1180 DoLink(global_dir, OutDir, "mail-forward.cdb")
1181 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1182 DoLink(global_dir, OutDir, "mail-disable")
1183 DoLink(global_dir, OutDir, "mail-greylist")
1184 DoLink(global_dir, OutDir, "mail-callout")
1185 DoLink(global_dir, OutDir, "mail-rbl")
1186 DoLink(global_dir, OutDir, "mail-rhsbl")
1187 DoLink(global_dir, OutDir, "mail-whitelist")
1188 DoLink(global_dir, OutDir, "all-accounts.json")
1189 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1190 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1191 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1194 DoLink(global_dir, OutDir, "forward-alias")
1196 if 'DNS' in ExtraList:
1197 DoLink(global_dir, OutDir, "dns-zone")
1198 DoLink(global_dir, OutDir, "dns-sshfp")
1200 if 'AUTHKEYS' in ExtraList:
1201 DoLink(global_dir, OutDir, "authorized_keys")
1203 if 'BSMTP' in ExtraList:
1204 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1206 if 'PRIVATE' in ExtraList:
1207 DoLink(global_dir, OutDir, "debian-private")
1209 if 'GITOLITE' in ExtraList:
1210 DoLink(global_dir, OutDir, "ssh-gitolite")
1212 if 'WEB-PASSWORDS' in ExtraList:
1213 DoLink(global_dir, OutDir, "web-passwords")
1215 if 'KEYRING' in ExtraList:
1217 bn = os.path.basename(k)
1218 if os.path.isdir(k):
1219 src = os.path.join(global_dir, bn)
1220 replaceTree(src, OutDir)
1222 DoLink(global_dir, OutDir, bn)
1226 bn = os.path.basename(k)
1227 target = os.path.join(OutDir, bn)
1228 if os.path.isdir(target):
1231 posix.remove(target)
1234 DoLink(global_dir, OutDir, "last_update.trace")
1237 def getLastLDAPChangeTime(l):
1238 mods = l.search_s('cn=log',
1239 ldap.SCOPE_ONELEVEL,
1240 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1245 # Sort the list by reqEnd
1246 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1247 # Take the last element in the array
1248 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1252 def getLastBuildTime(gdir):
1256 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1257 cache_last_mod=fd.read().split()
1259 cache_last_mod = cache_last_mod[0]
1264 if e.errno == errno.ENOENT:
1269 return cache_last_mod
1273 parser = optparse.OptionParser()
1274 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1275 help="Output directory.")
1276 parser.add_option("-f", "--force", dest="force", action="store_true",
1277 help="Force generation, even if not update to LDAP has happened.")
1279 (options, args) = parser.parse_args()
1284 if options.generatedir is not None:
1285 generate_dir = os.environ['UD_GENERATEDIR']
1286 elif 'UD_GENERATEDIR' in os.environ:
1287 generate_dir = os.environ['UD_GENERATEDIR']
1289 generate_dir = GenerateDir
1292 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1293 lock = get_lock( lockf )
1295 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1298 l = make_ldap_conn()
1300 ldap_last_mod = getLastLDAPChangeTime(l)
1301 cache_last_mod = getLastBuildTime(generate_dir)
1302 need_update = ldap_last_mod > cache_last_mod
1304 if not options.force and not need_update:
1305 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1306 fd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1310 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1311 generate_all(generate_dir, l)
1312 tracefd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1316 if __name__ == "__main__":
1317 if 'UD_PROFILE' in os.environ:
1320 cProfile.run('ud_generate()', "udg_prof")
1321 p = pstats.Stats('udg_prof')
1322 ##p.sort_stats('time').print_stats()
1323 p.sort_stats('cumulative').print_stats()
1329 # vim:set shiftwidth=3: