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])
426 contents = ssh_userkeys[f]
428 for line in contents:
429 if line.startswith("allowed_hosts=") and ' ' in line:
430 machines, line = line.split('=', 1)[1].split(' ', 1)
431 if CurrentHost not in machines.split(','):
432 continue # skip this key
435 continue # no keys for this host
436 contents = "\n".join(lines) + "\n"
438 to = tarfile.TarInfo(name=f)
439 # These will only be used where the username doesn't
440 # exist on the target system for some reason; hence,
441 # in those cases, the safest thing is for the file to
442 # be owned by root but group nobody. This deals with
443 # the bloody obscure case where the group fails to exist
444 # whilst the user does (in which case we want to avoid
445 # ending up with a file which is owned user:root to avoid
446 # a fairly obvious attack vector)
449 # Using the username / groupname fields avoids any need
450 # to give a shit^W^W^Wcare about the UIDoffset stuff.
454 to.mtime = int(time.time())
455 to.size = len(contents)
457 tf.addfile(to, StringIO(contents))
460 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
462 # add a list of groups to existing groups,
463 # including all subgroups thereof, recursively.
464 # basically this proceduces the transitive hull of the groups in
466 def addGroups(existingGroups, newGroups, uid):
467 for group in newGroups:
468 # if it's a <group>@host, split it and verify it's on the current host.
469 s = group.split('@', 1)
470 if len(s) == 2 and s[1] != CurrentHost:
474 # let's see if we handled this group already
475 if group in existingGroups:
478 if not GroupIDMap.has_key(group):
479 print "Group", group, "does not exist but", uid, "is in it"
482 existingGroups.append(group)
484 if SubGroupMap.has_key(group):
485 addGroups(existingGroups, SubGroupMap[group], uid)
487 # Generate the group list
488 def GenGroup(accounts, File):
492 F = open(File + ".tdb.tmp", "w")
494 # Generate the GroupMap
496 for x in GroupIDMap.keys():
498 GroupHasPrimaryMembers = {}
500 # Sort them into a list of groups having a set of users
502 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
503 if not 'supplementaryGid' in a: continue
506 addGroups(supgroups, a['supplementaryGid'], a['uid'])
508 GroupMap[g].append(a['uid'])
510 # Output the group file.
512 for x in GroupMap.keys():
513 if GroupIDMap.has_key(x) == 0:
516 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
519 grouprevmap[GroupIDMap[x]] = x
521 Line = "%s:x:%u:" % (x, GroupIDMap[x])
523 for I in GroupMap[x]:
524 Line = Line + ("%s%s" % (Comma, I))
526 Line = Sanitize(Line) + "\n"
527 F.write("0%u %s" % (J, Line))
528 F.write(".%s %s" % (x, Line))
529 F.write("=%u %s" % (GroupIDMap[x], Line))
532 # Oops, something unspeakable happened.
540 def CheckForward(accounts):
542 if not 'emailForward' in a: continue
546 # Do not allow people to try to buffer overflow busted parsers
547 if len(a['emailForward']) > 200: delete = True
548 # Check the forwarding address
549 elif EmailCheck.match(a['emailForward']) is None: delete = True
552 a.delete_mailforward()
554 # Generate the email forwarding list
555 def GenForward(accounts, File):
558 OldMask = os.umask(0022)
559 F = open(File + ".tmp", "w", 0644)
563 if not 'emailForward' in a: continue
564 Line = "%s: %s" % (a['uid'], a['emailForward'])
565 Line = Sanitize(Line) + "\n"
568 # Oops, something unspeakable happened.
574 def GenCDB(accounts, File, key):
577 OldMask = os.umask(0022)
578 Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
581 # Write out the email address for each user
583 if not key in a: continue
586 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
589 # Oops, something unspeakable happened.
593 if Fdb.close() != None:
594 raise "cdbmake gave an error"
596 # Generate the anon XEarth marker file
597 def GenMarkers(accounts, File):
600 F = open(File + ".tmp", "w")
602 # Write out the position for each user
604 if not ('latitude' in a and 'longitude' in a): continue
606 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
607 Line = Sanitize(Line) + "\n"
612 # Oops, something unspeakable happened.
618 # Generate the debian-private subscription list
619 def GenPrivate(accounts, File):
622 F = open(File + ".tmp", "w")
624 # Write out the position for each user
626 if not a.is_active_user(): continue
627 if not 'privateSub' in a: continue
629 Line = "%s"%(a['privateSub'])
630 Line = Sanitize(Line) + "\n"
635 # Oops, something unspeakable happened.
641 # Generate a list of locked accounts
642 def GenDisabledAccounts(accounts, File):
645 F = open(File + ".tmp", "w")
646 disabled_accounts = []
648 # Fetch all the users
650 if a.pw_active(): continue
651 Line = "%s:%s" % (a['uid'], "Account is locked")
652 disabled_accounts.append(a)
653 F.write(Sanitize(Line) + "\n")
655 # Oops, something unspeakable happened.
660 return disabled_accounts
662 # Generate the list of local addresses that refuse all mail
663 def GenMailDisable(accounts, File):
666 F = open(File + ".tmp", "w")
669 if not 'mailDisableMessage' in a: continue
670 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
671 Line = Sanitize(Line) + "\n"
674 # Oops, something unspeakable happened.
680 # Generate a list of uids that should have boolean affects applied
681 def GenMailBool(accounts, File, key):
684 F = open(File + ".tmp", "w")
687 if not key in a: continue
688 if not a[key] == 'TRUE': continue
689 Line = "%s"%(a['uid'])
690 Line = Sanitize(Line) + "\n"
693 # Oops, something unspeakable happened.
699 # Generate a list of hosts for RBL or whitelist purposes.
700 def GenMailList(accounts, File, key):
703 F = open(File + ".tmp", "w")
705 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
706 else: validregex = re.compile('^[-\w.]+$')
709 if not key in a: continue
711 filtered = filter(lambda z: validregex.match(z), a[key])
712 if len(filtered) == 0: continue
713 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
714 line = a['uid'] + ': ' + ' : '.join(filtered)
715 line = Sanitize(line) + "\n"
718 # Oops, something unspeakable happened.
724 def isRoleAccount(account):
725 return 'debianRoleAccount' in account['objectClass']
727 # Generate the DNS Zone file
728 def GenDNS(accounts, File):
731 F = open(File + ".tmp", "w")
733 # Fetch all the users
736 # Write out the zone file entry for each user
738 if not 'dnsZoneEntry' in a: continue
739 if not a.is_active_user() and not isRoleAccount(a): continue
742 F.write("; %s\n"%(a.email_address()))
743 for z in a["dnsZoneEntry"]:
744 Split = z.lower().split()
745 if Split[1].lower() == 'in':
746 Line = " ".join(Split) + "\n"
749 Host = Split[0] + DNSZone
750 if BSMTPCheck.match(Line) != None:
751 F.write("; Has BSMTP\n")
753 # Write some identification information
754 if not RRs.has_key(Host):
755 if Split[2].lower() in ["a", "aaaa"]:
756 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
757 for y in a["keyFingerPrint"]:
758 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
762 Line = "; Err %s"%(str(Split))
767 F.write("; Errors:\n")
768 for line in str(e).split("\n"):
769 F.write("; %s\n"%(line))
772 # Oops, something unspeakable happened.
778 def ExtractDNSInfo(x):
782 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
785 if x[1].has_key("ipHostNumber"):
786 for I in x[1]["ipHostNumber"]:
787 if IsV6Addr.match(I) != None:
788 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
790 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
794 if 'sshRSAHostKey' in x[1]:
795 for I in x[1]["sshRSAHostKey"]:
797 if Split[0] == 'ssh-rsa':
799 if Split[0] == 'ssh-dss':
801 if Algorithm == None:
803 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
804 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
806 if 'architecture' in x[1]:
807 Arch = GetAttr(x, "architecture")
809 if x[1].has_key("machine"):
810 Mach = " " + GetAttr(x, "machine")
811 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
813 if x[1].has_key("mXRecord"):
814 for I in x[1]["mXRecord"]:
815 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
819 # Generate the DNS records
820 def GenZoneRecords(host_attrs, File):
823 F = open(File + ".tmp", "w")
825 # Fetch all the hosts
827 if x[1].has_key("hostname") == 0:
830 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
833 DNSInfo = ExtractDNSInfo(x)
837 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
840 Line = "\t\t\t%s" % (Line)
844 # this would write sshfp lines for services on machines
845 # but we can't yet, since some are cnames and we'll make
846 # an invalid zonefile
848 # for i in x[1].get("purpose", []):
849 # m = PurposeHostField.match(i)
852 # # we ignore [[*..]] entries
853 # if m.startswith('*'):
855 # if m.startswith('-'):
858 # if not m.endswith(HostDomain):
860 # if not m.endswith('.'):
862 # for Line in DNSInfo:
863 # if isSSHFP.match(Line):
864 # Line = "%s\t%s" % (m, Line)
865 # F.write(Line + "\n")
867 # Oops, something unspeakable happened.
873 # Generate the BSMTP file
874 def GenBSMTP(accounts, File, HomePrefix):
877 F = open(File + ".tmp", "w")
879 # Write out the zone file entry for each user
881 if not 'dnsZoneEntry' in a: continue
882 if not a.is_active_user(): continue
885 for z in a["dnsZoneEntry"]:
886 Split = z.lower().split()
887 if Split[1].lower() == 'in':
888 for y in range(0, len(Split)):
891 Line = " ".join(Split) + "\n"
893 Host = Split[0] + DNSZone
894 if BSMTPCheck.match(Line) != None:
895 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
896 a['uid'], HomePrefix, a['uid'], Host))
899 F.write("; Errors\n")
902 # Oops, something unspeakable happened.
908 def HostToIP(Host, mapped=True):
912 if Host[1].has_key("ipHostNumber"):
913 for addr in Host[1]["ipHostNumber"]:
914 IPAdresses.append(addr)
915 if IsV6Addr.match(addr) is None and mapped == "True":
916 IPAdresses.append("::ffff:"+addr)
920 # Generate the ssh known hosts file
921 def GenSSHKnown(host_attrs, File, mode=None):
924 OldMask = os.umask(0022)
925 F = open(File + ".tmp", "w", 0644)
929 if x[1].has_key("hostname") == 0 or \
930 x[1].has_key("sshRSAHostKey") == 0:
932 Host = GetAttr(x, "hostname")
934 if Host.endswith(HostDomain):
935 HostNames.append(Host[:-(len(HostDomain) + 1)])
937 # in the purpose field [[host|some other text]] (where some other text is optional)
938 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
939 # file. But so that we don't have to add everything we link we can add an asterisk
940 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
941 # http linking it we also support [[-hostname]] entries.
942 for i in x[1].get("purpose", []):
943 m = PurposeHostField.match(i)
946 # we ignore [[*..]] entries
947 if m.startswith('*'):
949 if m.startswith('-'):
953 if m.endswith(HostDomain):
954 HostNames.append(m[:-(len(HostDomain) + 1)])
956 for I in x[1]["sshRSAHostKey"]:
957 if mode and mode == 'authorized_keys':
959 if 'sshdistAuthKeysHost' in x[1]:
960 hosts += x[1]['sshdistAuthKeysHost']
961 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)
963 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
964 Line = Sanitize(Line) + "\n"
966 # Oops, something unspeakable happened.
972 # Generate the debianhosts file (list of all IP addresses)
973 def GenHosts(host_attrs, File):
976 OldMask = os.umask(0022)
977 F = open(File + ".tmp", "w", 0644)
984 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
987 if not 'ipHostNumber' in x[1]:
990 addrs = x[1]["ipHostNumber"]
994 addr = Sanitize(addr) + "\n"
997 # Oops, something unspeakable happened.
1003 def replaceTree(src, dst_basedir):
1004 bn = os.path.basename(src)
1005 dst = os.path.join(dst_basedir, bn)
1007 shutil.copytree(src, dst)
1009 def GenKeyrings(OutDir):
1011 if os.path.isdir(k):
1012 replaceTree(k, OutDir)
1014 shutil.copy(k, OutDir)
1017 def get_accounts(ldap_conn):
1018 # Fetch all the users
1019 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1020 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1021 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1022 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1023 "shadowExpire", "emailForward", "latitude", "longitude",\
1024 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1025 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1026 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1027 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1028 "mailContentInspectionAction", "webPassword"])
1030 if passwd_attrs is None:
1031 raise UDEmptyList, "No Users"
1032 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1033 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1037 def get_hosts(ldap_conn):
1038 # Fetch all the hosts
1039 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1040 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1041 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1043 if HostAttrs == None:
1044 raise UDEmptyList, "No Hosts"
1046 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1051 def make_ldap_conn():
1052 # Connect to the ldap server
1054 # for testing purposes it's sometimes useful to pass username/password
1055 # via the environment
1056 if 'UD_CREDENTIALS' in os.environ:
1057 Pass = os.environ['UD_CREDENTIALS'].split()
1059 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1060 Pass = F.readline().strip().split(" ")
1062 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1066 def generate_all(global_dir, ldap_conn):
1067 accounts = get_accounts(ldap_conn)
1068 host_attrs = get_hosts(ldap_conn)
1071 # Generate global things
1072 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1074 accounts = filter(lambda x: not IsRetired(x), accounts)
1075 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1077 CheckForward(accounts)
1079 GenMailDisable(accounts, global_dir + "mail-disable")
1080 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1081 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1082 GenPrivate(accounts, global_dir + "debian-private")
1083 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1084 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1085 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1086 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1087 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1088 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1089 GenWebPassword(accounts, global_dir + "web-passwords")
1090 GenKeyrings(global_dir)
1093 GenForward(accounts, global_dir + "forward-alias")
1095 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1096 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1098 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1099 GenMarkers(accounts, global_dir + "markers")
1100 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1101 GenHosts(host_attrs, global_dir + "debianhosts")
1102 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1104 GenDNS(accounts, global_dir + "dns-zone")
1105 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1107 for host in host_attrs:
1108 if not "hostname" in host[1]:
1110 generate_host(host, global_dir, accounts, ssh_userkeys)
1112 def generate_host(host, global_dir, accounts, ssh_userkeys):
1115 CurrentHost = host[1]['hostname'][0]
1116 OutDir = global_dir + CurrentHost + '/'
1122 # Get the group list and convert any named groups to numerics
1124 for groupname in AllowedGroupsPreload.strip().split(" "):
1125 GroupList[groupname] = True
1126 if 'allowedGroups' in host[1]:
1127 for groupname in host[1]['allowedGroups']:
1128 GroupList[groupname] = True
1129 for groupname in GroupList.keys():
1130 if groupname in GroupIDMap:
1131 GroupList[str(GroupIDMap[groupname])] = True
1134 if 'exportOptions' in host[1]:
1135 for extra in host[1]['exportOptions']:
1136 ExtraList[extra.upper()] = True
1139 accounts = filter(lambda x: IsInGroup(x, GroupList), accounts)
1141 DoLink(global_dir, OutDir, "debianhosts")
1142 DoLink(global_dir, OutDir, "ssh_known_hosts")
1143 DoLink(global_dir, OutDir, "disabled-accounts")
1146 if 'NOPASSWD' in ExtraList:
1147 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1149 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1151 grouprevmap = GenGroup(accounts, OutDir + "group")
1152 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1154 # Now we know who we're allowing on the machine, export
1155 # the relevant ssh keys
1156 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1158 if not 'NOPASSWD' in ExtraList:
1159 GenShadow(accounts, OutDir + "shadow")
1161 # Link in global things
1162 if not 'NOMARKERS' in ExtraList:
1163 DoLink(global_dir, OutDir, "markers")
1164 DoLink(global_dir, OutDir, "mail-forward.cdb")
1165 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1166 DoLink(global_dir, OutDir, "mail-disable")
1167 DoLink(global_dir, OutDir, "mail-greylist")
1168 DoLink(global_dir, OutDir, "mail-callout")
1169 DoLink(global_dir, OutDir, "mail-rbl")
1170 DoLink(global_dir, OutDir, "mail-rhsbl")
1171 DoLink(global_dir, OutDir, "mail-whitelist")
1172 DoLink(global_dir, OutDir, "all-accounts.json")
1173 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1174 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1175 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1178 DoLink(global_dir, OutDir, "forward-alias")
1180 if 'DNS' in ExtraList:
1181 DoLink(global_dir, OutDir, "dns-zone")
1182 DoLink(global_dir, OutDir, "dns-sshfp")
1184 if 'AUTHKEYS' in ExtraList:
1185 DoLink(global_dir, OutDir, "authorized_keys")
1187 if 'BSMTP' in ExtraList:
1188 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1190 if 'PRIVATE' in ExtraList:
1191 DoLink(global_dir, OutDir, "debian-private")
1193 if 'GITOLITE' in ExtraList:
1194 DoLink(global_dir, OutDir, "ssh-gitolite")
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")
1221 def getLastLDAPChangeTime(l):
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]
1236 def getLastBuildTime():
1240 fd = open(os.path.join(GenerateDir, "last_update.trace"), "r")
1241 cache_last_mod=fd.read().split()
1243 cache_last_mod = cache_last_mod[0]
1248 if e.errno == errno.ENOENT:
1253 return cache_last_mod
1261 parser = optparse.OptionParser()
1262 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1263 help="Output directory.")
1264 parser.add_option("-f", "--force", dest="force", action="store_true",
1265 help="Force generation, even if not update to LDAP has happened.")
1267 (options, args) = parser.parse_args()
1273 l = make_ldap_conn()
1275 if options.generatedir is not None:
1276 GenerateDir = os.environ['UD_GENERATEDIR']
1277 elif 'UD_GENERATEDIR' in os.environ:
1278 GenerateDir = os.environ['UD_GENERATEDIR']
1280 ldap_last_mod = getLastLDAPChangeTime(l)
1281 cache_last_mod = getLastBuildTime()
1282 need_update = ldap_last_mod > cache_last_mod
1284 if not options.force and not need_update:
1285 fd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
1286 fd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1290 # Fetch all the groups
1292 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1293 ["gid", "gidNumber", "subGroup"])
1295 # Generate the SubGroupMap and GroupIDMap
1297 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1299 if x[1].has_key("gidNumber") == 0:
1301 GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1302 if x[1].has_key("subGroup") != 0:
1303 SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1307 lockf = os.path.join(GenerateDir, 'ud-generate.lock')
1308 lock = get_lock( lockf )
1310 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1313 tracefd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
1314 generate_all(GenerateDir, l)
1315 tracefd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1319 if lock is not None:
1322 if __name__ == "__main__":
1328 # vim:set shiftwidth=3: