3 # Generates passwd, shadow and group files from the ldap directory.
5 # Copyright (c) 2000-2001 Jason Gunthorpe <jgg@debian.org>
6 # Copyright (c) 2003-2004 James Troup <troup@debian.org>
7 # Copyright (c) 2004-2005,7 Joey Schulze <joey@infodrom.org>
8 # Copyright (c) 2001-2007 Ryan Murray <rmurray@debian.org>
9 # Copyright (c) 2008,2009,2010,2011 Peter Palfrader <peter@palfrader.org>
10 # Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
11 # Copyright (c) 2008 Mark Hymers <mhy@debian.org>
12 # Copyright (c) 2008 Luk Claes <luk@debian.org>
13 # Copyright (c) 2008 Thomas Viehmann <tv@beamnet.de>
14 # Copyright (c) 2009 Stephen Gran <steve@lobefin.net>
15 # Copyright (c) 2010 Helmut Grohne <helmut@subdivi.de>
17 # This program is free software; you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation; either version 2 of the License, or
20 # (at your option) any later version.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
31 import string, re, time, ldap, optparse, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp, fcntl
32 from userdir_ldap import *
33 from userdir_exceptions import *
35 from xml.etree.ElementTree import Element, SubElement, Comment
36 from xml.etree import ElementTree
37 from xml.dom import minidom
39 from cStringIO import StringIO
41 from StringIO import StringIO
43 import simplejson as json
46 if not '__author__' in json.__dict__:
47 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
48 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
51 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("^([^ <>@]+@[^ ,<>@]+)(,\s*([^ <>@]+@[^ ,<>@]+))*$")
66 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
67 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-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)
73 MX_remap = json.loads(ConfModule.MX_remap)
76 """Return a pretty-printed XML string for the Element.
78 rough_string = ElementTree.tostring(elem, 'utf-8')
79 reparsed = minidom.parseString(rough_string)
80 return reparsed.toprettyxml(indent=" ")
82 def safe_makedirs(dir):
86 if e.errno == errno.EEXIST:
95 if e.errno == errno.ENOENT:
100 def get_lock(fn, wait=5*60):
103 ends = time.time() + wait
108 fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
112 if time.time() >= ends:
114 sl = min(sl*2, 10, ends - time.time())
120 return Str.translate(string.maketrans("\n\r\t", "$$$"))
122 def DoLink(From, To, File):
124 posix.remove(To + File)
127 posix.link(From + File, To + File)
129 def IsRetired(account):
131 Looks for accountStatus in the LDAP record and tries to
132 match it against one of the known retired statuses
135 status = account['accountStatus']
137 line = status.split()
140 if status == "inactive":
143 elif status == "memorial":
146 elif status == "retiring":
147 # We'll give them a few extra days over what we said
148 age = 6 * 31 * 24 * 60 * 60
150 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
158 #def IsGidDebian(account):
159 # return account['gidNumber'] == 800
161 # See if this user is in the group list
162 def IsInGroup(account, allowed, current_host):
163 # See if the primary group is in the list
164 if str(account['gidNumber']) in allowed: return True
166 # Check the host based ACL
167 if account.is_allowed_by_hostacl(current_host): return True
169 # See if there are supplementary groups
170 if not 'supplementaryGid' in account: return False
173 addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
179 def Die(File, F, Fdb):
185 os.remove(File + ".tmp")
189 os.remove(File + ".tdb.tmp")
193 def Done(File, F, Fdb):
196 os.rename(File + ".tmp", File)
199 os.rename(File + ".tdb.tmp", File + ".tdb")
201 # Generate the password list
202 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
205 F = open(File + ".tdb.tmp", "w")
210 # Do not let people try to buffer overflow some busted passwd parser.
211 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
213 userlist[a['uid']] = a['gidNumber']
214 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
220 HomePrefix, a['uid'],
222 line = Sanitize(line) + "\n"
223 F.write("0%u %s" % (i, line))
224 F.write(".%s %s" % (a['uid'], line))
225 F.write("=%d %s" % (a['uidNumber'], line))
228 # Oops, something unspeakable happened.
234 # Return the list of users so we know which keys to export
237 def GenAllUsers(accounts, file):
240 OldMask = os.umask(0022)
241 f = open(file + ".tmp", "w", 0644)
246 all.append( { 'uid': a['uid'],
247 'uidNumber': a['uidNumber'],
248 'active': a.pw_active() and a.shadow_active() } )
251 # Oops, something unspeakable happened.
257 # Generate the shadow list
258 def GenShadow(accounts, File):
261 OldMask = os.umask(0077)
262 F = open(File + ".tdb.tmp", "w", 0600)
267 # If the account is locked, mark it as such in shadow
268 # See Debian Bug #308229 for why we set it to 1 instead of 0
269 if not a.pw_active(): ShadowExpire = '1'
270 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
271 else: ShadowExpire = ''
274 values.append(a['uid'])
275 values.append(a.get_password())
276 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
277 if key in a: values.append(a[key])
278 else: values.append('')
279 values.append(ShadowExpire)
280 line = ':'.join(values)+':'
281 line = Sanitize(line) + "\n"
282 F.write("0%u %s" % (i, line))
283 F.write(".%s %s" % (a['uid'], line))
286 # Oops, something unspeakable happened.
292 # Generate the sudo passwd file
293 def GenShadowSudo(accounts, File, untrusted, current_host):
296 OldMask = os.umask(0077)
297 F = open(File + ".tmp", "w", 0600)
302 if 'sudoPassword' in a:
303 for entry in a['sudoPassword']:
304 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
307 uuid = Match.group(1)
308 status = Match.group(2)
309 hosts = Match.group(3)
310 cryptedpass = Match.group(4)
312 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
314 for_all = hosts == "*"
315 for_this_host = current_host in hosts.split(',')
316 if not (for_all or for_this_host):
318 # ignore * passwords for untrusted hosts, but copy host specific passwords
319 if for_all and untrusted:
322 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
327 Line = "%s:%s" % (a['uid'], Pass)
328 Line = Sanitize(Line) + "\n"
329 F.write("%s" % (Line))
331 # Oops, something unspeakable happened.
337 # Generate the sudo passwd file
338 def GenSSHGitolite(accounts, File):
341 OldMask = os.umask(0022)
342 F = open(File + ".tmp", "w", 0600)
345 if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
347 if not 'sshRSAAuthKey' in a: continue
350 prefix = GitoliteSSHRestrictions.replace('@@USER@@', User)
351 for I in a["sshRSAAuthKey"]:
352 if I.startswith('ssh-'):
353 line = "%s %s"%(prefix, I)
355 line = "%s,%s"%(prefix, I)
356 line = Sanitize(line) + "\n"
359 # Oops, something unspeakable happened.
365 # Generate the shadow list
366 def GenSSHShadow(global_dir, accounts):
367 # Fetch all the users
371 if not 'sshRSAAuthKey' in a: continue
374 for I in a['sshRSAAuthKey']:
375 MultipleLine = "%s" % I
376 MultipleLine = Sanitize(MultipleLine)
377 contents.append(MultipleLine)
378 userkeys[a['uid']] = contents
381 # Generate the webPassword list
382 def GenWebPassword(accounts, File):
385 OldMask = os.umask(0077)
386 F = open(File, "w", 0600)
390 if not 'webPassword' in a: continue
391 if not a.pw_active(): continue
393 Pass = str(a['webPassword'])
394 Line = "%s:%s" % (a['uid'], Pass)
395 Line = Sanitize(Line) + "\n"
396 F.write("%s" % (Line))
402 # Generate the voipPassword list
403 def GenVoipPassword(accounts, File):
406 OldMask = os.umask(0077)
407 F = open(File, "w", 0600)
410 root = Element('include')
413 if not 'voipPassword' in a: continue
414 if not a.pw_active(): continue
416 Pass = str(a['voipPassword'])
417 user = Element('user')
418 user.attrib['id'] = "%s" % (a['uid'])
420 params = Element('params')
422 param = Element('param')
424 param.attrib['name'] = "a1-hash"
425 param.attrib['value'] = "%s" % (Pass)
426 variables = Element('variables')
427 user.append(variables)
428 variable = Element('variable')
429 variable.attrib['name'] = "toll_allow"
430 variable.attrib['value'] = "domestic,international,local"
431 variables.append(variable)
433 F.write("%s" % (prettify(root)))
440 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
441 OldMask = os.umask(0077)
442 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
445 if f not in ssh_userkeys:
447 # If we're not exporting their primary group, don't export
450 if userlist[f] in grouprevmap.keys():
451 grname = grouprevmap[userlist[f]]
454 if int(userlist[f]) <= 100:
455 # In these cases, look it up in the normal way so we
456 # deal with cases where, for instance, users are in group
457 # users as their primary group.
458 grname = grp.getgrgid(userlist[f])[0]
463 print "User %s is supposed to have their key exported to host %s but their primary group (gid: %d) isn't in LDAP" % (f, current_host, userlist[f])
467 for line in ssh_userkeys[f]:
468 if line.startswith("allowed_hosts=") and ' ' in line:
469 machines, line = line.split('=', 1)[1].split(' ', 1)
470 if current_host not in machines.split(','):
471 continue # skip this key
474 continue # no keys for this host
475 contents = "\n".join(lines) + "\n"
477 to = tarfile.TarInfo(name=f)
478 # These will only be used where the username doesn't
479 # exist on the target system for some reason; hence,
480 # in those cases, the safest thing is for the file to
481 # be owned by root but group nobody. This deals with
482 # the bloody obscure case where the group fails to exist
483 # whilst the user does (in which case we want to avoid
484 # ending up with a file which is owned user:root to avoid
485 # a fairly obvious attack vector)
488 # Using the username / groupname fields avoids any need
489 # to give a shit^W^W^Wcare about the UIDoffset stuff.
493 to.mtime = int(time.time())
494 to.size = len(contents)
496 tf.addfile(to, StringIO(contents))
499 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
501 # add a list of groups to existing groups,
502 # including all subgroups thereof, recursively.
503 # basically this proceduces the transitive hull of the groups in
505 def addGroups(existingGroups, newGroups, uid, current_host):
506 for group in newGroups:
507 # if it's a <group>@host, split it and verify it's on the current host.
508 s = group.split('@', 1)
509 if len(s) == 2 and s[1] != current_host:
513 # let's see if we handled this group already
514 if group in existingGroups:
517 if not GroupIDMap.has_key(group):
518 print "Group", group, "does not exist but", uid, "is in it"
521 existingGroups.append(group)
523 if SubGroupMap.has_key(group):
524 addGroups(existingGroups, SubGroupMap[group], uid, current_host)
526 # Generate the group list
527 def GenGroup(accounts, File, current_host):
531 F = open(File + ".tdb.tmp", "w")
533 # Generate the GroupMap
537 GroupHasPrimaryMembers = {}
539 # Sort them into a list of groups having a set of users
541 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
542 if not 'supplementaryGid' in a: continue
545 addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
547 GroupMap[g].append(a['uid'])
549 # Output the group file.
551 for x in GroupMap.keys():
552 if not x in GroupIDMap:
555 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
558 grouprevmap[GroupIDMap[x]] = x
560 Line = "%s:x:%u:" % (x, GroupIDMap[x])
562 for I in GroupMap[x]:
563 Line = Line + ("%s%s" % (Comma, I))
565 Line = Sanitize(Line) + "\n"
566 F.write("0%u %s" % (J, Line))
567 F.write(".%s %s" % (x, Line))
568 F.write("=%u %s" % (GroupIDMap[x], Line))
571 # Oops, something unspeakable happened.
579 def CheckForward(accounts):
581 if not 'emailForward' in a: continue
585 # Do not allow people to try to buffer overflow busted parsers
586 if len(a['emailForward']) > 200: delete = True
587 # Check the forwarding address
588 elif EmailCheck.match(a['emailForward']) is None: delete = True
591 a.delete_mailforward()
593 # Generate the email forwarding list
594 def GenForward(accounts, File):
597 OldMask = os.umask(0022)
598 F = open(File + ".tmp", "w", 0644)
602 if not 'emailForward' in a: continue
603 Line = "%s: %s" % (a['uid'], a['emailForward'])
604 Line = Sanitize(Line) + "\n"
607 # Oops, something unspeakable happened.
613 def GenCDB(accounts, File, key):
616 OldMask = os.umask(0022)
617 # nothing else does the fsync stuff, so why do it here?
618 prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
619 Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
622 # Write out the email address for each user
624 if not key in a: continue
627 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
630 # Oops, something unspeakable happened.
634 if Fdb.close() != None:
635 raise "cdbmake gave an error"
637 # Generate the anon XEarth marker file
638 def GenMarkers(accounts, File):
641 F = open(File + ".tmp", "w")
643 # Write out the position for each user
645 if not ('latitude' in a and 'longitude' in a): continue
647 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
648 Line = Sanitize(Line) + "\n"
653 # Oops, something unspeakable happened.
659 # Generate the debian-private subscription list
660 def GenPrivate(accounts, File):
663 F = open(File + ".tmp", "w")
665 # Write out the position for each user
667 if not a.is_active_user(): continue
668 if a.is_guest_account(): continue
669 if not 'privateSub' in a: continue
671 Line = "%s"%(a['privateSub'])
672 Line = Sanitize(Line) + "\n"
677 # Oops, something unspeakable happened.
683 # Generate a list of locked accounts
684 def GenDisabledAccounts(accounts, File):
687 F = open(File + ".tmp", "w")
688 disabled_accounts = []
690 # Fetch all the users
692 if a.pw_active(): continue
693 Line = "%s:%s" % (a['uid'], "Account is locked")
694 disabled_accounts.append(a)
695 F.write(Sanitize(Line) + "\n")
697 # Oops, something unspeakable happened.
702 return disabled_accounts
704 # Generate the list of local addresses that refuse all mail
705 def GenMailDisable(accounts, File):
708 F = open(File + ".tmp", "w")
711 if not 'mailDisableMessage' in a: continue
712 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
713 Line = Sanitize(Line) + "\n"
716 # Oops, something unspeakable happened.
722 # Generate a list of uids that should have boolean affects applied
723 def GenMailBool(accounts, File, key):
726 F = open(File + ".tmp", "w")
729 if not key in a: continue
730 if not a[key] == 'TRUE': continue
731 Line = "%s"%(a['uid'])
732 Line = Sanitize(Line) + "\n"
735 # Oops, something unspeakable happened.
741 # Generate a list of hosts for RBL or whitelist purposes.
742 def GenMailList(accounts, File, key):
745 F = open(File + ".tmp", "w")
747 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
748 else: validregex = re.compile('^[-\w.]+$')
751 if not key in a: continue
753 filtered = filter(lambda z: validregex.match(z), a[key])
754 if len(filtered) == 0: continue
755 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
756 line = a['uid'] + ': ' + ' : '.join(filtered)
757 line = Sanitize(line) + "\n"
760 # Oops, something unspeakable happened.
766 def isRoleAccount(account):
767 return 'debianRoleAccount' in account['objectClass']
769 # Generate the DNS Zone file
770 def GenDNS(accounts, File):
773 F = open(File + ".tmp", "w")
775 # Fetch all the users
778 # Write out the zone file entry for each user
780 if not 'dnsZoneEntry' in a: continue
781 if not a.is_active_user() and not isRoleAccount(a): continue
782 if a.is_guest_account(): continue
785 F.write("; %s\n"%(a.email_address()))
786 for z in a["dnsZoneEntry"]:
787 Split = z.lower().split()
788 if Split[1].lower() == 'in':
789 Line = " ".join(Split) + "\n"
792 Host = Split[0] + DNSZone
793 if BSMTPCheck.match(Line) != None:
794 F.write("; Has BSMTP\n")
796 # Write some identification information
797 if not RRs.has_key(Host):
798 if Split[2].lower() in ["a", "aaaa"]:
799 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
800 for y in a["keyFingerPrint"]:
801 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
805 Line = "; Err %s"%(str(Split))
810 F.write("; Errors:\n")
811 for line in str(e).split("\n"):
812 F.write("; %s\n"%(line))
815 # Oops, something unspeakable happened.
823 socket.inet_pton(socket.AF_INET6, i)
828 def ExtractDNSInfo(x):
832 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
835 if x[1].has_key("ipHostNumber"):
836 for I in x[1]["ipHostNumber"]:
838 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
840 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
844 if 'sshRSAHostKey' in x[1]:
845 for I in x[1]["sshRSAHostKey"]:
847 if Split[0] == 'ssh-rsa':
849 if Split[0] == 'ssh-dss':
851 if Algorithm == None:
853 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
854 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
856 if 'architecture' in x[1]:
857 Arch = GetAttr(x, "architecture")
859 if x[1].has_key("machine"):
860 Mach = " " + GetAttr(x, "machine")
861 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
863 if x[1].has_key("mXRecord"):
864 for I in x[1]["mXRecord"]:
866 for e in MX_remap[I]:
867 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
869 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
873 # Generate the DNS records
874 def GenZoneRecords(host_attrs, File):
877 F = open(File + ".tmp", "w")
879 # Fetch all the hosts
881 if x[1].has_key("hostname") == 0:
884 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
887 DNSInfo = ExtractDNSInfo(x)
891 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
894 Line = "\t\t\t%s" % (Line)
898 # this would write sshfp lines for services on machines
899 # but we can't yet, since some are cnames and we'll make
900 # an invalid zonefile
902 # for i in x[1].get("purpose", []):
903 # m = PurposeHostField.match(i)
906 # # we ignore [[*..]] entries
907 # if m.startswith('*'):
909 # if m.startswith('-'):
912 # if not m.endswith(HostDomain):
914 # if not m.endswith('.'):
916 # for Line in DNSInfo:
917 # if isSSHFP.match(Line):
918 # Line = "%s\t%s" % (m, Line)
919 # F.write(Line + "\n")
921 # Oops, something unspeakable happened.
927 # Generate the BSMTP file
928 def GenBSMTP(accounts, File, HomePrefix):
931 F = open(File + ".tmp", "w")
933 # Write out the zone file entry for each user
935 if not 'dnsZoneEntry' in a: continue
936 if not a.is_active_user(): continue
939 for z in a["dnsZoneEntry"]:
940 Split = z.lower().split()
941 if Split[1].lower() == 'in':
942 for y in range(0, len(Split)):
945 Line = " ".join(Split) + "\n"
947 Host = Split[0] + DNSZone
948 if BSMTPCheck.match(Line) != None:
949 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
950 a['uid'], HomePrefix, a['uid'], Host))
953 F.write("; Errors\n")
956 # Oops, something unspeakable happened.
962 def HostToIP(Host, mapped=True):
966 if Host[1].has_key("ipHostNumber"):
967 for addr in Host[1]["ipHostNumber"]:
968 IPAdresses.append(addr)
969 if not is_ipv6_addr(addr) and mapped == "True":
970 IPAdresses.append("::ffff:"+addr)
974 # Generate the ssh known hosts file
975 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
978 OldMask = os.umask(0022)
979 F = open(File + ".tmp", "w", 0644)
983 if x[1].has_key("hostname") == 0 or \
984 x[1].has_key("sshRSAHostKey") == 0:
986 Host = GetAttr(x, "hostname")
988 if Host.endswith(HostDomain):
989 HostNames.append(Host[:-(len(HostDomain) + 1)])
991 # in the purpose field [[host|some other text]] (where some other text is optional)
992 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
993 # file. But so that we don't have to add everything we link we can add an asterisk
994 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
995 # http linking it we also support [[-hostname]] entries.
996 for i in x[1].get("purpose", []):
997 m = PurposeHostField.match(i)
1000 # we ignore [[*..]] entries
1001 if m.startswith('*'):
1003 if m.startswith('-'):
1007 if m.endswith(HostDomain):
1008 HostNames.append(m[:-(len(HostDomain) + 1)])
1010 for I in x[1]["sshRSAHostKey"]:
1011 if mode and mode == 'authorized_keys':
1013 if 'sshdistAuthKeysHost' in x[1]:
1014 hosts += x[1]['sshdistAuthKeysHost']
1015 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1016 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1017 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1019 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1020 Line = Sanitize(Line) + "\n"
1022 # Oops, something unspeakable happened.
1028 # Generate the debianhosts file (list of all IP addresses)
1029 def GenHosts(host_attrs, File):
1032 OldMask = os.umask(0022)
1033 F = open(File + ".tmp", "w", 0644)
1038 for x in host_attrs:
1040 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1043 if not 'ipHostNumber' in x[1]:
1046 addrs = x[1]["ipHostNumber"]
1048 if addr not in seen:
1050 addr = Sanitize(addr) + "\n"
1053 # Oops, something unspeakable happened.
1059 def replaceTree(src, dst_basedir):
1060 bn = os.path.basename(src)
1061 dst = os.path.join(dst_basedir, bn)
1063 shutil.copytree(src, dst)
1065 def GenKeyrings(OutDir):
1067 if os.path.isdir(k):
1068 replaceTree(k, OutDir)
1070 shutil.copy(k, OutDir)
1073 def get_accounts(ldap_conn):
1074 # Fetch all the users
1075 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1076 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1077 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1078 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1079 "shadowExpire", "emailForward", "latitude", "longitude",\
1080 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1081 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1082 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1083 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1084 "mailContentInspectionAction", "webPassword", "voipPassword"])
1086 if passwd_attrs is None:
1087 raise UDEmptyList, "No Users"
1088 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1089 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1093 def get_hosts(ldap_conn):
1094 # Fetch all the hosts
1095 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1096 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1097 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1099 if HostAttrs == None:
1100 raise UDEmptyList, "No Hosts"
1102 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1107 def make_ldap_conn():
1108 # Connect to the ldap server
1110 # for testing purposes it's sometimes useful to pass username/password
1111 # via the environment
1112 if 'UD_CREDENTIALS' in os.environ:
1113 Pass = os.environ['UD_CREDENTIALS'].split()
1115 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1116 Pass = F.readline().strip().split(" ")
1118 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1124 def setup_group_maps(l):
1125 # Fetch all the groups
1128 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1129 ["gid", "gidNumber", "subGroup"])
1131 # Generate the subgroup_map and group_id_map
1133 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1135 if x[1].has_key("gidNumber") == 0:
1137 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1138 if x[1].has_key("subGroup") != 0:
1139 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1143 SubGroupMap = subgroup_map
1144 GroupIDMap = group_id_map
1146 def generate_all(global_dir, ldap_conn):
1147 accounts = get_accounts(ldap_conn)
1148 host_attrs = get_hosts(ldap_conn)
1151 # Generate global things
1152 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1154 accounts = filter(lambda x: not IsRetired(x), accounts)
1155 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1157 CheckForward(accounts)
1159 GenMailDisable(accounts, global_dir + "mail-disable")
1160 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1161 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1162 GenPrivate(accounts, global_dir + "debian-private")
1163 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1164 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1165 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1166 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1167 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1168 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1169 GenWebPassword(accounts, global_dir + "web-passwords")
1170 GenVoipPassword(accounts, global_dir + "voip-passwords")
1171 GenKeyrings(global_dir)
1174 GenForward(accounts, global_dir + "forward-alias")
1176 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1177 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1179 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1180 GenMarkers(accounts, global_dir + "markers")
1181 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1182 GenHosts(host_attrs, global_dir + "debianhosts")
1183 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1185 GenDNS(accounts, global_dir + "dns-zone")
1186 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1188 setup_group_maps(ldap_conn)
1190 for host in host_attrs:
1191 if not "hostname" in host[1]:
1193 generate_host(host, global_dir, accounts, ssh_userkeys)
1195 def generate_host(host, global_dir, all_accounts, ssh_userkeys):
1196 current_host = host[1]['hostname'][0]
1197 OutDir = global_dir + current_host + '/'
1198 if not os.path.isdir(OutDir):
1201 # Get the group list and convert any named groups to numerics
1203 for groupname in AllowedGroupsPreload.strip().split(" "):
1204 GroupList[groupname] = True
1205 if 'allowedGroups' in host[1]:
1206 for groupname in host[1]['allowedGroups']:
1207 GroupList[groupname] = True
1208 for groupname in GroupList.keys():
1209 if groupname in GroupIDMap:
1210 GroupList[str(GroupIDMap[groupname])] = True
1213 if 'exportOptions' in host[1]:
1214 for extra in host[1]['exportOptions']:
1215 ExtraList[extra.upper()] = True
1218 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1220 DoLink(global_dir, OutDir, "debianhosts")
1221 DoLink(global_dir, OutDir, "ssh_known_hosts")
1222 DoLink(global_dir, OutDir, "disabled-accounts")
1225 if 'NOPASSWD' in ExtraList:
1226 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1228 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1230 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1231 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1233 # Now we know who we're allowing on the machine, export
1234 # the relevant ssh keys
1235 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1237 if not 'NOPASSWD' in ExtraList:
1238 GenShadow(accounts, OutDir + "shadow")
1240 # Link in global things
1241 if not 'NOMARKERS' in ExtraList:
1242 DoLink(global_dir, OutDir, "markers")
1243 DoLink(global_dir, OutDir, "mail-forward.cdb")
1244 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1245 DoLink(global_dir, OutDir, "mail-disable")
1246 DoLink(global_dir, OutDir, "mail-greylist")
1247 DoLink(global_dir, OutDir, "mail-callout")
1248 DoLink(global_dir, OutDir, "mail-rbl")
1249 DoLink(global_dir, OutDir, "mail-rhsbl")
1250 DoLink(global_dir, OutDir, "mail-whitelist")
1251 DoLink(global_dir, OutDir, "all-accounts.json")
1252 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1253 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1254 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1257 DoLink(global_dir, OutDir, "forward-alias")
1259 if 'DNS' in ExtraList:
1260 DoLink(global_dir, OutDir, "dns-zone")
1261 DoLink(global_dir, OutDir, "dns-sshfp")
1263 if 'AUTHKEYS' in ExtraList:
1264 DoLink(global_dir, OutDir, "authorized_keys")
1266 if 'BSMTP' in ExtraList:
1267 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1269 if 'PRIVATE' in ExtraList:
1270 DoLink(global_dir, OutDir, "debian-private")
1272 if 'GITOLITE' in ExtraList:
1273 DoLink(global_dir, OutDir, "ssh-gitolite")
1274 if 'exportOptions' in host[1]:
1275 for entry in host[1]['exportOptions']:
1276 v = entry.split('=',1)
1277 if v[0] != 'GITOLITE' or len(v) != 2: continue
1278 gitolite_accounts = filter(lambda x: IsInGroup(x, [v[1]], current_host), all_accounts)
1279 GenSSHGitolite(gitolite_accounts, OutDir + "ssh-gitolite-%s"%(v[1],))
1281 if 'WEB-PASSWORDS' in ExtraList:
1282 DoLink(global_dir, OutDir, "web-passwords")
1284 if 'VOIP-PASSWORDS' in ExtraList:
1285 DoLink(global_dir, OutDir, "voip-passwords")
1287 if 'KEYRING' in ExtraList:
1289 bn = os.path.basename(k)
1290 if os.path.isdir(k):
1291 src = os.path.join(global_dir, bn)
1292 replaceTree(src, OutDir)
1294 DoLink(global_dir, OutDir, bn)
1298 bn = os.path.basename(k)
1299 target = os.path.join(OutDir, bn)
1300 if os.path.isdir(target):
1303 posix.remove(target)
1306 DoLink(global_dir, OutDir, "last_update.trace")
1309 def getLastLDAPChangeTime(l):
1310 mods = l.search_s('cn=log',
1311 ldap.SCOPE_ONELEVEL,
1312 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1317 # Sort the list by reqEnd
1318 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1319 # Take the last element in the array
1320 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1324 def getLastKeyringChangeTime():
1327 mt = os.path.getmtime(k)
1333 def getLastBuildTime(gdir):
1334 cache_last_ldap_mod = 0
1335 cache_last_unix_mod = 0
1338 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1339 cache_last_mod=fd.read().split()
1341 cache_last_ldap_mod = cache_last_mod[0]
1342 cache_last_unix_mod = int(cache_last_mod[1])
1343 except IndexError, ValueError:
1347 if e.errno == errno.ENOENT:
1352 return (cache_last_ldap_mod, cache_last_unix_mod)
1355 parser = optparse.OptionParser()
1356 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1357 help="Output directory.")
1358 parser.add_option("-f", "--force", dest="force", action="store_true",
1359 help="Force generation, even if no update to LDAP has happened.")
1361 (options, args) = parser.parse_args()
1366 if options.generatedir is not None:
1367 generate_dir = os.environ['UD_GENERATEDIR']
1368 elif 'UD_GENERATEDIR' in os.environ:
1369 generate_dir = os.environ['UD_GENERATEDIR']
1371 generate_dir = GenerateDir
1374 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1375 lock = get_lock( lockf )
1377 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1380 l = make_ldap_conn()
1382 time_started = int(time.time())
1383 ldap_last_mod = getLastLDAPChangeTime(l)
1384 unix_last_mod = getLastKeyringChangeTime()
1385 cache_last_ldap_mod, cache_last_unix_mod = getLastBuildTime(generate_dir)
1387 need_update = (ldap_last_mod > cache_last_ldap_mod) or (unix_last_mod > cache_last_unix_mod)
1389 if not options.force and not need_update:
1390 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1391 fd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1395 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1396 generate_all(generate_dir, l)
1397 tracefd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1401 if __name__ == "__main__":
1402 if 'UD_PROFILE' in os.environ:
1405 cProfile.run('ud_generate()', "udg_prof")
1406 p = pstats.Stats('udg_prof')
1407 ##p.sort_stats('time').print_stats()
1408 p.sort_stats('cumulative').print_stats()
1414 # vim:set shiftwidth=3: