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")
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(":")
73 GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
76 def safe_makedirs(dir):
80 if e.errno == errno.EEXIST:
89 if e.errno == errno.ENOENT:
94 def get_lock(fn, wait=5*60, max_age=3600*6):
96 stat = os.stat(fn + '.lock')
97 if stat.st_mtime < time.time() - max_age:
98 sys.stderr.write("Removing stale lock %s"%(fn + '.lock'))
99 os.unlink(fn + '.lock')
100 except OSError, error:
101 if error.errno == errno.ENOENT:
106 lock = lockfile.FileLock(fn)
108 lock.acquire(timeout=wait)
109 except lockfile.LockTimeout:
116 return Str.translate(string.maketrans("\n\r\t", "$$$"))
118 def DoLink(From, To, File):
120 posix.remove(To + File)
123 posix.link(From + File, To + File)
125 def IsRetired(account):
127 Looks for accountStatus in the LDAP record and tries to
128 match it against one of the known retired statuses
131 status = account['accountStatus']
133 line = status.split()
136 if status == "inactive":
139 elif status == "memorial":
142 elif status == "retiring":
143 # We'll give them a few extra days over what we said
144 age = 6 * 31 * 24 * 60 * 60
146 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
154 #def IsGidDebian(account):
155 # return account['gidNumber'] == 800
157 # See if this user is in the group list
158 def IsInGroup(account):
162 # See if the primary group is in the list
163 if str(account['gidNumber']) in Allowed: return True
165 # Check the host based ACL
166 if account.is_allowed_by_hostacl(CurrentHost): return True
168 # See if there are supplementary groups
169 if not 'supplementaryGid' in account: return False
172 addGroups(supgroups, account['supplementaryGid'], account['uid'])
174 if Allowed.has_key(g):
178 def Die(File, F, Fdb):
184 os.remove(File + ".tmp")
188 os.remove(File + ".tdb.tmp")
192 def Done(File, F, Fdb):
195 os.rename(File + ".tmp", File)
198 os.rename(File + ".tdb.tmp", File + ".tdb")
200 # Generate the password list
201 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
204 F = open(File + ".tdb.tmp", "w")
209 if not IsInGroup(a): continue
211 # Do not let people try to buffer overflow some busted passwd parser.
212 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
214 userlist[a['uid']] = a['gidNumber']
215 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
221 HomePrefix, a['uid'],
223 line = Sanitize(line) + "\n"
224 F.write("0%u %s" % (i, line))
225 F.write(".%s %s" % (a['uid'], line))
226 F.write("=%d %s" % (a['uidNumber'], line))
229 # Oops, something unspeakable happened.
235 # Return the list of users so we know which keys to export
238 def GenAllUsers(accounts, file):
241 OldMask = os.umask(0022)
242 f = open(file + ".tmp", "w", 0644)
247 all.append( { 'uid': a['uid'],
248 'uidNumber': a['uidNumber'],
249 'active': a.pw_active() and a.shadow_active() } )
252 # Oops, something unspeakable happened.
258 # Generate the shadow list
259 def GenShadow(accounts, File):
262 OldMask = os.umask(0077)
263 F = open(File + ".tdb.tmp", "w", 0600)
269 if not IsInGroup(a): continue
271 # If the account is locked, mark it as such in shadow
272 # See Debian Bug #308229 for why we set it to 1 instead of 0
273 if not a.pw_active(): ShadowExpire = '1'
274 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
275 else: ShadowExpire = ''
278 values.append(a['uid'])
279 values.append(a.get_password())
280 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
281 if key in a: values.append(a[key])
282 else: values.append('')
283 values.append(ShadowExpire)
284 line = ':'.join(values)+':'
285 line = Sanitize(line) + "\n"
286 F.write("0%u %s" % (i, line))
287 F.write(".%s %s" % (a['uid'], line))
290 # Oops, something unspeakable happened.
296 # Generate the sudo passwd file
297 def GenShadowSudo(accounts, File, untrusted):
300 OldMask = os.umask(0077)
301 F = open(File + ".tmp", "w", 0600)
306 if not IsInGroup(a): continue
308 if 'sudoPassword' in a:
309 for entry in a['sudoPassword']:
310 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
313 uuid = Match.group(1)
314 status = Match.group(2)
315 hosts = Match.group(3)
316 cryptedpass = Match.group(4)
318 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
320 for_all = hosts == "*"
321 for_this_host = CurrentHost in hosts.split(',')
322 if not (for_all or for_this_host):
324 # ignore * passwords for untrusted hosts, but copy host specific passwords
325 if for_all and untrusted:
328 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
333 Line = "%s:%s" % (a['uid'], Pass)
334 Line = Sanitize(Line) + "\n"
335 F.write("%s" % (Line))
337 # Oops, something unspeakable happened.
343 # Generate the sudo passwd file
344 def GenSSHGitolite(accounts, File):
347 OldMask = os.umask(0022)
348 F = open(File + ".tmp", "w", 0600)
351 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
353 if not 'sshRSAAuthKey' in a: continue
356 prefix = GitoliteSSHRestrictions.replace('@@USER@@', User)
357 for I in a["sshRSAAuthKey"]:
358 if I.startswith('ssh-'):
359 line = "%s %s"%(prefix, I)
361 line = "%s,%s"%(prefix, I)
362 line = Sanitize(line) + "\n"
365 # Oops, something unspeakable happened.
371 # Generate the shadow list
372 def GenSSHShadow(global_dir, accounts):
373 # Fetch all the users
376 safe_rmtree(os.path.join(global_dir, 'userkeys'))
377 safe_makedirs(os.path.join(global_dir, 'userkeys'))
380 if not 'sshRSAAuthKey' in a: continue
384 OldMask = os.umask(0077)
385 File = os.path.join(global_dir, 'userkeys', a['uid'])
386 F = open(File + ".tmp", "w", 0600)
389 for I in a['sshRSAAuthKey']:
390 MultipleLine = "%s" % I
391 MultipleLine = Sanitize(MultipleLine) + "\n"
392 F.write(MultipleLine)
395 userfiles.append(os.path.basename(File))
397 # Oops, something unspeakable happened.
400 # As neither masterFileName nor masterFile are defined at any point
401 # this will raise a NameError.
402 Die(masterFileName, masterFile, None)
407 # Generate the webPassword list
408 def GenWebPassword(accounts, File):
411 OldMask = os.umask(0077)
412 F = open(File, "w", 0600)
416 if not 'webPassword' in a: continue
417 if not a.pw_active(): continue
419 Pass = str(a['webPassword'])
420 Line = "%s:%s" % (a['uid'], Pass)
421 Line = Sanitize(Line) + "\n"
422 F.write("%s" % (Line))
428 def GenSSHtarballs(global_dir, userlist, SSHFiles, grouprevmap, target):
429 OldMask = os.umask(0077)
430 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
432 for f in userlist.keys():
433 if f not in SSHFiles:
435 # If we're not exporting their primary group, don't export
438 if userlist[f] in grouprevmap.keys():
439 grname = grouprevmap[userlist[f]]
442 if int(userlist[f]) <= 100:
443 # In these cases, look it up in the normal way so we
444 # deal with cases where, for instance, users are in group
445 # users as their primary group.
446 grname = grp.getgrgid(userlist[f])[0]
451 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])
454 to = tf.gettarinfo(os.path.join(global_dir, 'userkeys', f), f)
455 # These will only be used where the username doesn't
456 # exist on the target system for some reason; hence,
457 # in those cases, the safest thing is for the file to
458 # be owned by root but group nobody. This deals with
459 # the bloody obscure case where the group fails to exist
460 # whilst the user does (in which case we want to avoid
461 # ending up with a file which is owned user:root to avoid
462 # a fairly obvious attack vector)
465 # Using the username / groupname fields avoids any need
466 # to give a shit^W^W^Wcare about the UIDoffset stuff.
471 contents = file(os.path.join(global_dir, 'userkeys', f)).read()
473 for line in contents.splitlines():
474 if line.startswith("allowed_hosts=") and ' ' in line:
475 machines, line = line.split('=', 1)[1].split(' ', 1)
476 if CurrentHost not in machines.split(','):
477 continue # skip this key
480 continue # no keys for this host
481 contents = "\n".join(lines) + "\n"
482 to.size = len(contents)
483 tf.addfile(to, StringIO(contents))
486 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
488 # add a list of groups to existing groups,
489 # including all subgroups thereof, recursively.
490 # basically this proceduces the transitive hull of the groups in
492 def addGroups(existingGroups, newGroups, uid):
493 for group in newGroups:
494 # if it's a <group>@host, split it and verify it's on the current host.
495 s = group.split('@', 1)
496 if len(s) == 2 and s[1] != CurrentHost:
500 # let's see if we handled this group already
501 if group in existingGroups:
504 if not GroupIDMap.has_key(group):
505 print "Group", group, "does not exist but", uid, "is in it"
508 existingGroups.append(group)
510 if SubGroupMap.has_key(group):
511 addGroups(existingGroups, SubGroupMap[group], uid)
513 # Generate the group list
514 def GenGroup(accounts, File):
518 F = open(File + ".tdb.tmp", "w")
520 # Generate the GroupMap
522 for x in GroupIDMap.keys():
524 GroupHasPrimaryMembers = {}
526 # Sort them into a list of groups having a set of users
528 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
529 if not IsInGroup(a): continue
530 if not 'supplementaryGid' in a: continue
533 addGroups(supgroups, a['supplementaryGid'], a['uid'])
535 GroupMap[g].append(a['uid'])
537 # Output the group file.
539 for x in GroupMap.keys():
540 if GroupIDMap.has_key(x) == 0:
543 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
546 grouprevmap[GroupIDMap[x]] = x
548 Line = "%s:x:%u:" % (x, GroupIDMap[x])
550 for I in GroupMap[x]:
551 Line = Line + ("%s%s" % (Comma, I))
553 Line = Sanitize(Line) + "\n"
554 F.write("0%u %s" % (J, Line))
555 F.write(".%s %s" % (x, Line))
556 F.write("=%u %s" % (GroupIDMap[x], Line))
559 # Oops, something unspeakable happened.
567 def CheckForward(accounts):
569 if not 'emailForward' in a: continue
574 if not IsInGroup(a): delete = True
575 # Do not allow people to try to buffer overflow busted parsers
576 elif len(a['emailForward']) > 200: delete = True
577 # Check the forwarding address
578 elif EmailCheck.match(a['emailForward']) is None: delete = True
581 a.delete_mailforward()
583 # Generate the email forwarding list
584 def GenForward(accounts, File):
587 OldMask = os.umask(0022)
588 F = open(File + ".tmp", "w", 0644)
592 if not 'emailForward' in a: continue
593 Line = "%s: %s" % (a['uid'], a['emailForward'])
594 Line = Sanitize(Line) + "\n"
597 # Oops, something unspeakable happened.
603 def GenCDB(accounts, File, key):
606 OldMask = os.umask(0022)
607 Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
610 # Write out the email address for each user
612 if not key in a: continue
615 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
618 # Oops, something unspeakable happened.
622 if Fdb.close() != None:
623 raise "cdbmake gave an error"
625 # Generate the anon XEarth marker file
626 def GenMarkers(accounts, File):
629 F = open(File + ".tmp", "w")
631 # Write out the position for each user
633 if not ('latitude' in a and 'longitude' in a): continue
635 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
636 Line = Sanitize(Line) + "\n"
641 # Oops, something unspeakable happened.
647 # Generate the debian-private subscription list
648 def GenPrivate(accounts, File):
651 F = open(File + ".tmp", "w")
653 # Write out the position for each user
655 if not a.is_active_user(): continue
656 if not 'privateSub' in a: continue
658 Line = "%s"%(a['privateSub'])
659 Line = Sanitize(Line) + "\n"
664 # Oops, something unspeakable happened.
670 # Generate a list of locked accounts
671 def GenDisabledAccounts(accounts, File):
674 F = open(File + ".tmp", "w")
675 disabled_accounts = []
677 # Fetch all the users
679 if a.pw_active(): continue
680 Line = "%s:%s" % (a['uid'], "Account is locked")
681 disabled_accounts.append(a)
682 F.write(Sanitize(Line) + "\n")
684 # Oops, something unspeakable happened.
689 return disabled_accounts
691 # Generate the list of local addresses that refuse all mail
692 def GenMailDisable(accounts, File):
695 F = open(File + ".tmp", "w")
698 if not 'mailDisableMessage' in a: continue
699 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
700 Line = Sanitize(Line) + "\n"
703 # Oops, something unspeakable happened.
709 # Generate a list of uids that should have boolean affects applied
710 def GenMailBool(accounts, File, key):
713 F = open(File + ".tmp", "w")
716 if not key in a: continue
717 if not a[key] == 'TRUE': continue
718 Line = "%s"%(a['uid'])
719 Line = Sanitize(Line) + "\n"
722 # Oops, something unspeakable happened.
728 # Generate a list of hosts for RBL or whitelist purposes.
729 def GenMailList(accounts, File, key):
732 F = open(File + ".tmp", "w")
734 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
735 else: validregex = re.compile('^[-\w.]+$')
738 if not key in a: continue
740 filtered = filter(lambda z: validregex.match(z), a[key])
741 if len(filtered) == 0: continue
742 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
743 line = a['uid'] + ': ' + ' : '.join(filtered)
744 line = Sanitize(line) + "\n"
747 # Oops, something unspeakable happened.
753 def isRoleAccount(account):
754 return 'debianRoleAccount' in account['objectClass']
756 # Generate the DNS Zone file
757 def GenDNS(accounts, File):
760 F = open(File + ".tmp", "w")
762 # Fetch all the users
765 # Write out the zone file entry for each user
767 if not 'dnsZoneEntry' in a: continue
768 if not a.is_active_user() and not isRoleAccount(a): continue
771 F.write("; %s\n"%(a.email_address()))
772 for z in a["dnsZoneEntry"]:
773 Split = z.lower().split()
774 if Split[1].lower() == 'in':
775 Line = " ".join(Split) + "\n"
778 Host = Split[0] + DNSZone
779 if BSMTPCheck.match(Line) != None:
780 F.write("; Has BSMTP\n")
782 # Write some identification information
783 if not RRs.has_key(Host):
784 if Split[2].lower() in ["a", "aaaa"]:
785 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
786 for y in a["keyFingerPrint"]:
787 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
791 Line = "; Err %s"%(str(Split))
796 F.write("; Errors:\n")
797 for line in str(e).split("\n"):
798 F.write("; %s\n"%(line))
801 # Oops, something unspeakable happened.
807 def ExtractDNSInfo(x):
811 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
814 if x[1].has_key("ipHostNumber"):
815 for I in x[1]["ipHostNumber"]:
816 if IsV6Addr.match(I) != None:
817 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
819 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
823 if 'sshRSAHostKey' in x[1]:
824 for I in x[1]["sshRSAHostKey"]:
826 if Split[0] == 'ssh-rsa':
828 if Split[0] == 'ssh-dss':
830 if Algorithm == None:
832 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
833 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
835 if 'architecture' in x[1]:
836 Arch = GetAttr(x, "architecture")
838 if x[1].has_key("machine"):
839 Mach = " " + GetAttr(x, "machine")
840 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
842 if x[1].has_key("mXRecord"):
843 for I in x[1]["mXRecord"]:
844 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
848 # Generate the DNS records
849 def GenZoneRecords(host_attrs, File):
852 F = open(File + ".tmp", "w")
854 # Fetch all the hosts
856 if x[1].has_key("hostname") == 0:
859 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
862 DNSInfo = ExtractDNSInfo(x)
866 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
869 Line = "\t\t\t%s" % (Line)
873 # this would write sshfp lines for services on machines
874 # but we can't yet, since some are cnames and we'll make
875 # an invalid zonefile
877 # for i in x[1].get("purpose", []):
878 # m = PurposeHostField.match(i)
881 # # we ignore [[*..]] entries
882 # if m.startswith('*'):
884 # if m.startswith('-'):
887 # if not m.endswith(HostDomain):
889 # if not m.endswith('.'):
891 # for Line in DNSInfo:
892 # if isSSHFP.match(Line):
893 # Line = "%s\t%s" % (m, Line)
894 # F.write(Line + "\n")
896 # Oops, something unspeakable happened.
902 # Generate the BSMTP file
903 def GenBSMTP(accounts, File, HomePrefix):
906 F = open(File + ".tmp", "w")
908 # Write out the zone file entry for each user
910 if not 'dnsZoneEntry' in a: continue
911 if not a.is_active_user(): continue
914 for z in a["dnsZoneEntry"]:
915 Split = z.lower().split()
916 if Split[1].lower() == 'in':
917 for y in range(0, len(Split)):
920 Line = " ".join(Split) + "\n"
922 Host = Split[0] + DNSZone
923 if BSMTPCheck.match(Line) != None:
924 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
925 a['uid'], HomePrefix, a['uid'], Host))
928 F.write("; Errors\n")
931 # Oops, something unspeakable happened.
937 def HostToIP(Host, mapped=True):
941 if Host[1].has_key("ipHostNumber"):
942 for addr in Host[1]["ipHostNumber"]:
943 IPAdresses.append(addr)
944 if IsV6Addr.match(addr) is None and mapped == "True":
945 IPAdresses.append("::ffff:"+addr)
949 # Generate the ssh known hosts file
950 def GenSSHKnown(host_attrs, File, mode=None):
953 OldMask = os.umask(0022)
954 F = open(File + ".tmp", "w", 0644)
958 if x[1].has_key("hostname") == 0 or \
959 x[1].has_key("sshRSAHostKey") == 0:
961 Host = GetAttr(x, "hostname")
963 if Host.endswith(HostDomain):
964 HostNames.append(Host[:-(len(HostDomain) + 1)])
966 # in the purpose field [[host|some other text]] (where some other text is optional)
967 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
968 # file. But so that we don't have to add everything we link we can add an asterisk
969 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
970 # http linking it we also support [[-hostname]] entries.
971 for i in x[1].get("purpose", []):
972 m = PurposeHostField.match(i)
975 # we ignore [[*..]] entries
976 if m.startswith('*'):
978 if m.startswith('-'):
982 if m.endswith(HostDomain):
983 HostNames.append(m[:-(len(HostDomain) + 1)])
985 for I in x[1]["sshRSAHostKey"]:
986 if mode and mode == 'authorized_keys':
988 if 'sshdistAuthKeysHost' in x[1]:
989 hosts += x[1]['sshdistAuthKeysHost']
990 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)
992 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
993 Line = Sanitize(Line) + "\n"
995 # Oops, something unspeakable happened.
1001 # Generate the debianhosts file (list of all IP addresses)
1002 def GenHosts(host_attrs, File):
1005 OldMask = os.umask(0022)
1006 F = open(File + ".tmp", "w", 0644)
1011 for x in host_attrs:
1013 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1016 if not 'ipHostNumber' in x[1]:
1019 addrs = x[1]["ipHostNumber"]
1021 if addr not in seen:
1023 addr = Sanitize(addr) + "\n"
1026 # Oops, something unspeakable happened.
1032 def replaceTree(src, dst_basedir):
1033 bn = os.path.basename(src)
1034 dst = os.path.join(dst_basedir, bn)
1036 shutil.copytree(src, dst)
1038 def GenKeyrings(OutDir):
1040 if os.path.isdir(k):
1041 replaceTree(k, OutDir)
1043 shutil.copy(k, OutDir)
1046 def get_accounts(ldap_conn):
1047 # Fetch all the users
1048 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1049 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1050 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1051 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1052 "shadowExpire", "emailForward", "latitude", "longitude",\
1053 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1054 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1055 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1056 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1057 "mailContentInspectionAction", "webPassword"])
1059 if passwd_attrs is None:
1060 raise UDEmptyList, "No Users"
1061 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1062 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1066 def get_hosts(ldap_conn):
1067 # Fetch all the hosts
1068 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1069 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1070 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1072 if HostAttrs == None:
1073 raise UDEmptyList, "No Hosts"
1075 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1080 def make_ldap_conn():
1081 # Connect to the ldap server
1083 # for testing purposes it's sometimes useful to pass username/password
1084 # via the environment
1085 if 'UD_CREDENTIALS' in os.environ:
1086 Pass = os.environ['UD_CREDENTIALS'].split()
1088 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1089 Pass = F.readline().strip().split(" ")
1091 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1095 def generate_all(global_dir, ldap_conn):
1096 accounts = get_accounts(ldap_conn)
1097 host_attrs = get_hosts(ldap_conn)
1100 # Generate global things
1101 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1103 accounts = filter(lambda x: not IsRetired(x), accounts)
1104 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1106 CheckForward(accounts)
1108 GenMailDisable(accounts, global_dir + "mail-disable")
1109 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1110 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1111 GenPrivate(accounts, global_dir + "debian-private")
1112 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1113 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1114 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1115 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1116 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1117 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1118 GenWebPassword(accounts, global_dir + "web-passwords")
1119 GenKeyrings(global_dir)
1122 GenForward(accounts, global_dir + "forward-alias")
1124 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1125 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1127 ssh_files = GenSSHShadow(global_dir, accounts)
1128 GenMarkers(accounts, global_dir + "markers")
1129 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1130 GenHosts(host_attrs, global_dir + "debianhosts")
1131 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1133 GenDNS(accounts, global_dir + "dns-zone")
1134 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1136 for host in host_attrs:
1137 if not "hostname" in host[1]:
1139 generate_host(host, global_dir, accounts, ssh_files)
1141 def generate_host(host, global_dir, accounts, ssh_files):
1144 CurrentHost = host[1]['hostname'][0]
1145 OutDir = global_dir + CurrentHost + '/'
1151 # Get the group list and convert any named groups to numerics
1153 for groupname in AllowedGroupsPreload.strip().split(" "):
1154 GroupList[groupname] = True
1155 if 'allowedGroups' in host[1]:
1156 for groupname in host[1]['allowedGroups']:
1157 GroupList[groupname] = True
1158 for groupname in GroupList.keys():
1159 if groupname in GroupIDMap:
1160 GroupList[str(GroupIDMap[groupname])] = True
1163 if 'exportOptions' in host[1]:
1164 for extra in host[1]['exportOptions']:
1165 ExtraList[extra.upper()] = True
1172 DoLink(global_dir, OutDir, "debianhosts")
1173 DoLink(global_dir, OutDir, "ssh_known_hosts")
1174 DoLink(global_dir, OutDir, "disabled-accounts")
1177 if 'NOPASSWD' in ExtraList:
1178 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1180 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1182 grouprevmap = GenGroup(accounts, OutDir + "group")
1183 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1185 # Now we know who we're allowing on the machine, export
1186 # the relevant ssh keys
1187 GenSSHtarballs(global_dir, userlist, ssh_files, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1189 if not 'NOPASSWD' in ExtraList:
1190 GenShadow(accounts, OutDir + "shadow")
1192 # Link in global things
1193 if not 'NOMARKERS' in ExtraList:
1194 DoLink(global_dir, OutDir, "markers")
1195 DoLink(global_dir, OutDir, "mail-forward.cdb")
1196 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1197 DoLink(global_dir, OutDir, "mail-disable")
1198 DoLink(global_dir, OutDir, "mail-greylist")
1199 DoLink(global_dir, OutDir, "mail-callout")
1200 DoLink(global_dir, OutDir, "mail-rbl")
1201 DoLink(global_dir, OutDir, "mail-rhsbl")
1202 DoLink(global_dir, OutDir, "mail-whitelist")
1203 DoLink(global_dir, OutDir, "all-accounts.json")
1204 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "user-forward.cdb", 'emailForward')
1205 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "batv-tokens.cdb", 'bATVToken')
1206 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1209 DoLink(global_dir, OutDir, "forward-alias")
1211 if 'DNS' in ExtraList:
1212 DoLink(global_dir, OutDir, "dns-zone")
1213 DoLink(global_dir, OutDir, "dns-sshfp")
1215 if 'AUTHKEYS' in ExtraList:
1216 DoLink(global_dir, OutDir, "authorized_keys")
1218 if 'BSMTP' in ExtraList:
1219 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1221 if 'PRIVATE' in ExtraList:
1222 DoLink(global_dir, OutDir, "debian-private")
1224 if 'GITOLITE' in ExtraList:
1225 DoLink(global_dir, OutDir, "ssh-gitolite")
1227 if 'WEB-PASSWORDS' in ExtraList:
1228 DoLink(global_dir, OutDir, "web-passwords")
1230 if 'KEYRING' in ExtraList:
1232 bn = os.path.basename(k)
1233 if os.path.isdir(k):
1234 src = os.path.join(global_dir, bn)
1235 replaceTree(src, OutDir)
1237 DoLink(global_dir, OutDir, bn)
1241 bn = os.path.basename(k)
1242 target = os.path.join(OutDir, bn)
1243 if os.path.isdir(target):
1246 posix.remove(target)
1249 DoLink(global_dir, OutDir, "last_update.trace")
1252 def getLastLDAPChangeTime(l):
1253 mods = l.search_s('cn=log',
1254 ldap.SCOPE_ONELEVEL,
1255 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1260 # Sort the list by reqEnd
1261 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1262 # Take the last element in the array
1263 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1267 def getLastBuildTime():
1271 fd = open(os.path.join(GenerateDir, "last_update.trace"), "r")
1272 cache_last_mod=fd.read().split()
1274 cache_last_mod = cache_last_mod[0]
1279 if e.errno == errno.ENOENT:
1284 return cache_last_mod
1289 parser = optparse.OptionParser()
1290 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1291 help="Output directory.")
1292 parser.add_option("-f", "--force", dest="force", action="store_true",
1293 help="Force generation, even if not update to LDAP has happened.")
1295 (options, args) = parser.parse_args()
1301 l = make_ldap_conn()
1303 if options.generatedir is not None:
1304 GenerateDir = os.environ['UD_GENERATEDIR']
1305 elif 'UD_GENERATEDIR' in os.environ:
1306 GenerateDir = os.environ['UD_GENERATEDIR']
1308 ldap_last_mod = getLastLDAPChangeTime(l)
1309 cache_last_mod = getLastBuildTime()
1310 need_update = ldap_last_mod > cache_last_mod
1312 if not options.force and not need_update:
1313 fd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
1314 fd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1318 # Fetch all the groups
1320 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1321 ["gid", "gidNumber", "subGroup"])
1323 # Generate the SubGroupMap and GroupIDMap
1325 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1327 if x[1].has_key("gidNumber") == 0:
1329 GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1330 if x[1].has_key("subGroup") != 0:
1331 SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1335 lockf = os.path.join(GenerateDir, 'ud-generate.lock')
1336 lock = get_lock( lockf )
1338 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1341 tracefd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
1342 generate_all(GenerateDir, l)
1343 tracefd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
1347 if lock is not None:
1353 # vim:set shiftwidth=3: