3 # Generates passwd, shadow and group files from the ldap directory.
5 # Copyright (c) 2000-2001 Jason Gunthorpe <jgg@debian.org>
6 # Copyright (c) 2003-2004 James Troup <troup@debian.org>
7 # Copyright (c) 2004-2005,7 Joey Schulze <joey@infodrom.org>
8 # Copyright (c) 2001-2007 Ryan Murray <rmurray@debian.org>
9 # Copyright (c) 2008,2009,2010,2011 Peter Palfrader <peter@palfrader.org>
10 # Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
11 # Copyright (c) 2008 Mark Hymers <mhy@debian.org>
12 # Copyright (c) 2008 Luk Claes <luk@debian.org>
13 # Copyright (c) 2008 Thomas Viehmann <tv@beamnet.de>
14 # Copyright (c) 2009 Stephen Gran <steve@lobefin.net>
15 # Copyright (c) 2010 Helmut Grohne <helmut@subdivi.de>
17 # This program is free software; you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation; either version 2 of the License, or
20 # (at your option) any later version.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
31 import string, re, time, ldap, getopt, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp
33 from userdir_ldap import *
34 from userdir_exceptions import *
37 from cStringIO import StringIO
39 from StringIO import StringIO
41 import simplejson as json
44 if not '__author__' in json.__dict__:
45 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
46 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
49 sys.stderr.write("You should probably not run ud-generate as root.\n")
63 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
65 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
66 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
67 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
68 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
69 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
70 isSSHFP = re.compile("^\s*IN\s+SSHFP")
71 DNSZone = ".debian.net"
72 Keyrings = ConfModule.sync_keyrings.split(":")
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 for y in range(0, len(Split)):
778 Line = " ".join(Split) + "\n"
781 Host = Split[0] + DNSZone
782 if BSMTPCheck.match(Line) != None:
783 F.write("; Has BSMTP\n")
785 # Write some identification information
786 if not RRs.has_key(Host):
787 if Split[2].lower() in ["a", "aaaa"]:
788 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
789 for y in a["keyFingerPrint"]:
790 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
794 Line = "; Err %s"%(str(Split))
799 F.write("; Errors:\n")
800 for line in str(e).split("\n"):
801 F.write("; %s\n"%(line))
804 # Oops, something unspeakable happened.
810 def ExtractDNSInfo(x):
814 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
817 if x[1].has_key("ipHostNumber"):
818 for I in x[1]["ipHostNumber"]:
819 if IsV6Addr.match(I) != None:
820 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
822 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
826 if 'sshRSAHostKey' in x[1]:
827 for I in x[1]["sshRSAHostKey"]:
829 if Split[0] == 'ssh-rsa':
831 if Split[0] == 'ssh-dss':
833 if Algorithm == None:
835 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
836 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
838 if 'architecture' in x[1]:
839 Arch = GetAttr(x, "architecture")
841 if x[1].has_key("machine"):
842 Mach = " " + GetAttr(x, "machine")
843 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
845 if x[1].has_key("mXRecord"):
846 for I in x[1]["mXRecord"]:
847 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
851 # Generate the DNS records
852 def GenZoneRecords(host_attrs, File):
855 F = open(File + ".tmp", "w")
857 # Fetch all the hosts
859 if x[1].has_key("hostname") == 0:
862 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
865 DNSInfo = ExtractDNSInfo(x)
869 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
872 Line = "\t\t\t%s" % (Line)
876 # this would write sshfp lines for services on machines
877 # but we can't yet, since some are cnames and we'll make
878 # an invalid zonefile
880 # for i in x[1].get("purpose", []):
881 # m = PurposeHostField.match(i)
884 # # we ignore [[*..]] entries
885 # if m.startswith('*'):
887 # if m.startswith('-'):
890 # if not m.endswith(HostDomain):
892 # if not m.endswith('.'):
894 # for Line in DNSInfo:
895 # if isSSHFP.match(Line):
896 # Line = "%s\t%s" % (m, Line)
897 # F.write(Line + "\n")
899 # Oops, something unspeakable happened.
905 # Generate the BSMTP file
906 def GenBSMTP(accounts, File, HomePrefix):
909 F = open(File + ".tmp", "w")
911 # Write out the zone file entry for each user
913 if not 'dnsZoneEntry' in a: continue
914 if not a.is_active_user(): continue
917 for z in a["dnsZoneEntry"]:
918 Split = z.lower().split()
919 if Split[1].lower() == 'in':
920 for y in range(0, len(Split)):
923 Line = " ".join(Split) + "\n"
925 Host = Split[0] + DNSZone
926 if BSMTPCheck.match(Line) != None:
927 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
928 a['uid'], HomePrefix, a['uid'], Host))
931 F.write("; Errors\n")
934 # Oops, something unspeakable happened.
940 def HostToIP(Host, mapped=True):
944 if Host[1].has_key("ipHostNumber"):
945 for addr in Host[1]["ipHostNumber"]:
946 IPAdresses.append(addr)
947 if IsV6Addr.match(addr) is None and mapped == "True":
948 IPAdresses.append("::ffff:"+addr)
952 # Generate the ssh known hosts file
953 def GenSSHKnown(host_attrs, File, mode=None):
956 OldMask = os.umask(0022)
957 F = open(File + ".tmp", "w", 0644)
961 if x[1].has_key("hostname") == 0 or \
962 x[1].has_key("sshRSAHostKey") == 0:
964 Host = GetAttr(x, "hostname")
966 if Host.endswith(HostDomain):
967 HostNames.append(Host[:-(len(HostDomain) + 1)])
969 # in the purpose field [[host|some other text]] (where some other text is optional)
970 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
971 # file. But so that we don't have to add everything we link we can add an asterisk
972 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
973 # http linking it we also support [[-hostname]] entries.
974 for i in x[1].get("purpose", []):
975 m = PurposeHostField.match(i)
978 # we ignore [[*..]] entries
979 if m.startswith('*'):
981 if m.startswith('-'):
985 if m.endswith(HostDomain):
986 HostNames.append(m[:-(len(HostDomain) + 1)])
988 for I in x[1]["sshRSAHostKey"]:
989 if mode and mode == 'authorized_keys':
991 if 'sshdistAuthKeysHost' in x[1]:
992 hosts += x[1]['sshdistAuthKeysHost']
993 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)
995 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
996 Line = Sanitize(Line) + "\n"
998 # Oops, something unspeakable happened.
1004 # Generate the debianhosts file (list of all IP addresses)
1005 def GenHosts(host_attrs, File):
1008 OldMask = os.umask(0022)
1009 F = open(File + ".tmp", "w", 0644)
1014 for x in host_attrs:
1016 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1019 if not 'ipHostNumber' in x[1]:
1022 addrs = x[1]["ipHostNumber"]
1024 if addr not in seen:
1026 addr = Sanitize(addr) + "\n"
1029 # Oops, something unspeakable happened.
1035 def replaceTree(src, dst_basedir):
1036 bn = os.path.basename(src)
1037 dst = os.path.join(dst_basedir, bn)
1039 shutil.copytree(src, dst)
1041 def GenKeyrings(OutDir):
1043 if os.path.isdir(k):
1044 replaceTree(k, OutDir)
1046 shutil.copy(k, OutDir)
1049 def get_accounts(ldap_conn):
1050 # Fetch all the users
1051 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1052 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1053 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1054 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1055 "shadowExpire", "emailForward", "latitude", "longitude",\
1056 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1057 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1058 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1059 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1060 "mailContentInspectionAction", "webPassword"])
1062 if passwd_attrs is None:
1063 raise UDEmptyList, "No Users"
1064 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1065 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1069 def get_hosts(ldap_conn):
1070 # Fetch all the hosts
1071 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1072 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1073 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1075 if HostAttrs == None:
1076 raise UDEmptyList, "No Hosts"
1078 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1083 def make_ldap_conn():
1084 # Connect to the ldap server
1086 # for testing purposes it's sometimes useful to pass username/password
1087 # via the environment
1088 if 'UD_CREDENTIALS' in os.environ:
1089 Pass = os.environ['UD_CREDENTIALS'].split()
1091 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1092 Pass = F.readline().strip().split(" ")
1094 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1098 def generate_all(global_dir, ldap_conn):
1099 accounts = get_accounts(ldap_conn)
1100 host_attrs = get_hosts(ldap_conn)
1103 # Generate global things
1104 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1106 accounts = filter(lambda x: not IsRetired(x), accounts)
1107 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1109 CheckForward(accounts)
1111 GenMailDisable(accounts, global_dir + "mail-disable")
1112 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1113 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1114 GenPrivate(accounts, global_dir + "debian-private")
1115 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1116 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1117 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1118 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1119 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1120 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1121 GenWebPassword(accounts, global_dir + "web-passwords")
1122 GenKeyrings(global_dir)
1125 GenForward(accounts, global_dir + "forward-alias")
1127 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1128 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1130 ssh_files = GenSSHShadow(global_dir, accounts)
1131 GenMarkers(accounts, global_dir + "markers")
1132 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1133 GenHosts(host_attrs, global_dir + "debianhosts")
1134 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1136 GenDNS(accounts, global_dir + "dns-zone")
1137 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1139 for host in host_attrs:
1140 if not "hostname" in host[1]:
1142 generate_host(host, global_dir, accounts, ssh_files)
1144 def generate_host(host, global_dir, accounts, ssh_files):
1147 CurrentHost = host[1]['hostname'][0]
1148 OutDir = global_dir + CurrentHost + '/'
1154 # Get the group list and convert any named groups to numerics
1156 for groupname in AllowedGroupsPreload.strip().split(" "):
1157 GroupList[groupname] = True
1158 if 'allowedGroups' in host[1]:
1159 for groupname in host[1]['allowedGroups']:
1160 GroupList[groupname] = True
1161 for groupname in GroupList.keys():
1162 if groupname in GroupIDMap:
1163 GroupList[str(GroupIDMap[groupname])] = True
1166 if 'exportOptions' in host[1]:
1167 for extra in host[1]['exportOptions']:
1168 ExtraList[extra.upper()] = True
1175 DoLink(global_dir, OutDir, "debianhosts")
1176 DoLink(global_dir, OutDir, "ssh_known_hosts")
1177 DoLink(global_dir, OutDir, "disabled-accounts")
1180 if 'NOPASSWD' in ExtraList:
1181 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1183 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1185 grouprevmap = GenGroup(accounts, OutDir + "group")
1186 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1188 # Now we know who we're allowing on the machine, export
1189 # the relevant ssh keys
1190 GenSSHtarballs(global_dir, userlist, ssh_files, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1192 if not 'NOPASSWD' in ExtraList:
1193 GenShadow(accounts, OutDir + "shadow")
1195 # Link in global things
1196 if not 'NOMARKERS' in ExtraList:
1197 DoLink(global_dir, OutDir, "markers")
1198 DoLink(global_dir, OutDir, "mail-forward.cdb")
1199 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1200 DoLink(global_dir, OutDir, "mail-disable")
1201 DoLink(global_dir, OutDir, "mail-greylist")
1202 DoLink(global_dir, OutDir, "mail-callout")
1203 DoLink(global_dir, OutDir, "mail-rbl")
1204 DoLink(global_dir, OutDir, "mail-rhsbl")
1205 DoLink(global_dir, OutDir, "mail-whitelist")
1206 DoLink(global_dir, OutDir, "all-accounts.json")
1207 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "user-forward.cdb", 'emailForward')
1208 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "batv-tokens.cdb", 'bATVToken')
1209 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1212 DoLink(global_dir, OutDir, "forward-alias")
1214 if 'DNS' in ExtraList:
1215 DoLink(global_dir, OutDir, "dns-zone")
1216 DoLink(global_dir, OutDir, "dns-sshfp")
1218 if 'AUTHKEYS' in ExtraList:
1219 DoLink(global_dir, OutDir, "authorized_keys")
1221 if 'BSMTP' in ExtraList:
1222 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1224 if 'PRIVATE' in ExtraList:
1225 DoLink(global_dir, OutDir, "debian-private")
1227 if 'GITOLITE' in ExtraList:
1228 DoLink(global_dir, OutDir, "ssh-gitolite")
1230 if 'WEB-PASSWORDS' in ExtraList:
1231 DoLink(global_dir, OutDir, "web-passwords")
1233 if 'KEYRING' in ExtraList:
1235 bn = os.path.basename(k)
1236 if os.path.isdir(k):
1237 src = os.path.join(global_dir, bn)
1238 replaceTree(src, OutDir)
1240 DoLink(global_dir, OutDir, bn)
1244 bn = os.path.basename(k)
1245 target = os.path.join(OutDir, bn)
1246 if os.path.isdir(target):
1249 posix.remove(target)
1252 DoLink(global_dir, OutDir, "last_update.trace")
1254 l = make_ldap_conn()
1256 mods = l.search_s('cn=log',
1257 ldap.SCOPE_ONELEVEL,
1258 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1263 # Sort the list by reqEnd
1264 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1265 # Take the last element in the array
1266 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1268 # override globaldir for testing
1269 if 'UD_GENERATEDIR' in os.environ:
1270 GenerateDir = os.environ['UD_GENERATEDIR']
1275 fd = open(os.path.join(GenerateDir, "last_update.trace"), "r")
1276 cache_last_mod=fd.read().split()
1278 cache_last_mod = cache_last_mod[0]
1283 if e.errno == errno.ENOENT:
1288 if cache_last_mod >= last:
1289 fd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
1290 fd.write("%s\n%s\n" % (last, int(time.time())))
1294 # Fetch all the groups
1296 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1297 ["gid", "gidNumber", "subGroup"])
1299 # Generate the SubGroupMap and GroupIDMap
1301 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1303 if x[1].has_key("gidNumber") == 0:
1305 GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1306 if x[1].has_key("subGroup") != 0:
1307 SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1311 lockf = os.path.join(GenerateDir, 'ud-generate.lock')
1312 lock = get_lock( lockf )
1314 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1317 tracefd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
1318 generate_all(GenerateDir, l)
1319 tracefd.write("%s\n%s\n" % (last, int(time.time())))
1323 if lock is not None:
1329 # vim:set shiftwidth=3: