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 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 """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.
821 def ExtractDNSInfo(x):
825 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
828 if x[1].has_key("ipHostNumber"):
829 for I in x[1]["ipHostNumber"]:
830 if IsV6Addr.match(I) != None:
831 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
833 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
837 if 'sshRSAHostKey' in x[1]:
838 for I in x[1]["sshRSAHostKey"]:
840 if Split[0] == 'ssh-rsa':
842 if Split[0] == 'ssh-dss':
844 if Algorithm == None:
846 Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
847 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
849 if 'architecture' in x[1]:
850 Arch = GetAttr(x, "architecture")
852 if x[1].has_key("machine"):
853 Mach = " " + GetAttr(x, "machine")
854 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
856 if x[1].has_key("mXRecord"):
857 for I in x[1]["mXRecord"]:
858 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
862 # Generate the DNS records
863 def GenZoneRecords(host_attrs, File):
866 F = open(File + ".tmp", "w")
868 # Fetch all the hosts
870 if x[1].has_key("hostname") == 0:
873 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
876 DNSInfo = ExtractDNSInfo(x)
880 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
883 Line = "\t\t\t%s" % (Line)
887 # this would write sshfp lines for services on machines
888 # but we can't yet, since some are cnames and we'll make
889 # an invalid zonefile
891 # for i in x[1].get("purpose", []):
892 # m = PurposeHostField.match(i)
895 # # we ignore [[*..]] entries
896 # if m.startswith('*'):
898 # if m.startswith('-'):
901 # if not m.endswith(HostDomain):
903 # if not m.endswith('.'):
905 # for Line in DNSInfo:
906 # if isSSHFP.match(Line):
907 # Line = "%s\t%s" % (m, Line)
908 # F.write(Line + "\n")
910 # Oops, something unspeakable happened.
916 # Generate the BSMTP file
917 def GenBSMTP(accounts, File, HomePrefix):
920 F = open(File + ".tmp", "w")
922 # Write out the zone file entry for each user
924 if not 'dnsZoneEntry' in a: continue
925 if not a.is_active_user(): continue
928 for z in a["dnsZoneEntry"]:
929 Split = z.lower().split()
930 if Split[1].lower() == 'in':
931 for y in range(0, len(Split)):
934 Line = " ".join(Split) + "\n"
936 Host = Split[0] + DNSZone
937 if BSMTPCheck.match(Line) != None:
938 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
939 a['uid'], HomePrefix, a['uid'], Host))
942 F.write("; Errors\n")
945 # Oops, something unspeakable happened.
951 def HostToIP(Host, mapped=True):
955 if Host[1].has_key("ipHostNumber"):
956 for addr in Host[1]["ipHostNumber"]:
957 IPAdresses.append(addr)
958 if IsV6Addr.match(addr) is None and mapped == "True":
959 IPAdresses.append("::ffff:"+addr)
963 # Generate the ssh known hosts file
964 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
967 OldMask = os.umask(0022)
968 F = open(File + ".tmp", "w", 0644)
972 if x[1].has_key("hostname") == 0 or \
973 x[1].has_key("sshRSAHostKey") == 0:
975 Host = GetAttr(x, "hostname")
977 if Host.endswith(HostDomain):
978 HostNames.append(Host[:-(len(HostDomain) + 1)])
980 # in the purpose field [[host|some other text]] (where some other text is optional)
981 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
982 # file. But so that we don't have to add everything we link we can add an asterisk
983 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
984 # http linking it we also support [[-hostname]] entries.
985 for i in x[1].get("purpose", []):
986 m = PurposeHostField.match(i)
989 # we ignore [[*..]] entries
990 if m.startswith('*'):
992 if m.startswith('-'):
996 if m.endswith(HostDomain):
997 HostNames.append(m[:-(len(HostDomain) + 1)])
999 for I in x[1]["sshRSAHostKey"]:
1000 if mode and mode == 'authorized_keys':
1002 if 'sshdistAuthKeysHost' in x[1]:
1003 hosts += x[1]['sshdistAuthKeysHost']
1004 clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1005 clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1006 Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1008 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1009 Line = Sanitize(Line) + "\n"
1011 # Oops, something unspeakable happened.
1017 # Generate the debianhosts file (list of all IP addresses)
1018 def GenHosts(host_attrs, File):
1021 OldMask = os.umask(0022)
1022 F = open(File + ".tmp", "w", 0644)
1027 for x in host_attrs:
1029 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1032 if not 'ipHostNumber' in x[1]:
1035 addrs = x[1]["ipHostNumber"]
1037 if addr not in seen:
1039 addr = Sanitize(addr) + "\n"
1042 # Oops, something unspeakable happened.
1048 def replaceTree(src, dst_basedir):
1049 bn = os.path.basename(src)
1050 dst = os.path.join(dst_basedir, bn)
1052 shutil.copytree(src, dst)
1054 def GenKeyrings(OutDir):
1056 if os.path.isdir(k):
1057 replaceTree(k, OutDir)
1059 shutil.copy(k, OutDir)
1062 def get_accounts(ldap_conn):
1063 # Fetch all the users
1064 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1065 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1066 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1067 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1068 "shadowExpire", "emailForward", "latitude", "longitude",\
1069 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1070 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1071 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1072 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1073 "mailContentInspectionAction", "webPassword", "voipPassword"])
1075 if passwd_attrs is None:
1076 raise UDEmptyList, "No Users"
1077 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1078 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1082 def get_hosts(ldap_conn):
1083 # Fetch all the hosts
1084 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1085 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1086 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1088 if HostAttrs == None:
1089 raise UDEmptyList, "No Hosts"
1091 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1096 def make_ldap_conn():
1097 # Connect to the ldap server
1099 # for testing purposes it's sometimes useful to pass username/password
1100 # via the environment
1101 if 'UD_CREDENTIALS' in os.environ:
1102 Pass = os.environ['UD_CREDENTIALS'].split()
1104 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1105 Pass = F.readline().strip().split(" ")
1107 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1113 def setup_group_maps(l):
1114 # Fetch all the groups
1117 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1118 ["gid", "gidNumber", "subGroup"])
1120 # Generate the subgroup_map and group_id_map
1122 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1124 if x[1].has_key("gidNumber") == 0:
1126 group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1127 if x[1].has_key("subGroup") != 0:
1128 subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1132 SubGroupMap = subgroup_map
1133 GroupIDMap = group_id_map
1135 def generate_all(global_dir, ldap_conn):
1136 accounts = get_accounts(ldap_conn)
1137 host_attrs = get_hosts(ldap_conn)
1140 # Generate global things
1141 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1143 accounts = filter(lambda x: not IsRetired(x), accounts)
1144 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1146 CheckForward(accounts)
1148 GenMailDisable(accounts, global_dir + "mail-disable")
1149 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1150 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1151 GenPrivate(accounts, global_dir + "debian-private")
1152 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1153 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1154 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1155 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1156 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1157 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1158 GenWebPassword(accounts, global_dir + "web-passwords")
1159 GenVoipPassword(accounts, global_dir + "voip-passwords")
1160 GenKeyrings(global_dir)
1163 GenForward(accounts, global_dir + "forward-alias")
1165 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1166 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1168 ssh_userkeys = GenSSHShadow(global_dir, accounts)
1169 GenMarkers(accounts, global_dir + "markers")
1170 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1171 GenHosts(host_attrs, global_dir + "debianhosts")
1172 GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1174 GenDNS(accounts, global_dir + "dns-zone")
1175 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1177 setup_group_maps(ldap_conn)
1179 for host in host_attrs:
1180 if not "hostname" in host[1]:
1182 generate_host(host, global_dir, accounts, ssh_userkeys)
1184 def generate_host(host, global_dir, all_accounts, ssh_userkeys):
1185 current_host = host[1]['hostname'][0]
1186 OutDir = global_dir + current_host + '/'
1187 if not os.path.isdir(OutDir):
1190 # Get the group list and convert any named groups to numerics
1192 for groupname in AllowedGroupsPreload.strip().split(" "):
1193 GroupList[groupname] = True
1194 if 'allowedGroups' in host[1]:
1195 for groupname in host[1]['allowedGroups']:
1196 GroupList[groupname] = True
1197 for groupname in GroupList.keys():
1198 if groupname in GroupIDMap:
1199 GroupList[str(GroupIDMap[groupname])] = True
1202 if 'exportOptions' in host[1]:
1203 for extra in host[1]['exportOptions']:
1204 ExtraList[extra.upper()] = True
1207 accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1209 DoLink(global_dir, OutDir, "debianhosts")
1210 DoLink(global_dir, OutDir, "ssh_known_hosts")
1211 DoLink(global_dir, OutDir, "disabled-accounts")
1214 if 'NOPASSWD' in ExtraList:
1215 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1217 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1219 grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1220 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1222 # Now we know who we're allowing on the machine, export
1223 # the relevant ssh keys
1224 GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1226 if not 'NOPASSWD' in ExtraList:
1227 GenShadow(accounts, OutDir + "shadow")
1229 # Link in global things
1230 if not 'NOMARKERS' in ExtraList:
1231 DoLink(global_dir, OutDir, "markers")
1232 DoLink(global_dir, OutDir, "mail-forward.cdb")
1233 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1234 DoLink(global_dir, OutDir, "mail-disable")
1235 DoLink(global_dir, OutDir, "mail-greylist")
1236 DoLink(global_dir, OutDir, "mail-callout")
1237 DoLink(global_dir, OutDir, "mail-rbl")
1238 DoLink(global_dir, OutDir, "mail-rhsbl")
1239 DoLink(global_dir, OutDir, "mail-whitelist")
1240 DoLink(global_dir, OutDir, "all-accounts.json")
1241 GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1242 GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1243 GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1246 DoLink(global_dir, OutDir, "forward-alias")
1248 if 'DNS' in ExtraList:
1249 DoLink(global_dir, OutDir, "dns-zone")
1250 DoLink(global_dir, OutDir, "dns-sshfp")
1252 if 'AUTHKEYS' in ExtraList:
1253 DoLink(global_dir, OutDir, "authorized_keys")
1255 if 'BSMTP' in ExtraList:
1256 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1258 if 'PRIVATE' in ExtraList:
1259 DoLink(global_dir, OutDir, "debian-private")
1261 if 'GITOLITE' in ExtraList:
1262 DoLink(global_dir, OutDir, "ssh-gitolite")
1264 if 'WEB-PASSWORDS' in ExtraList:
1265 DoLink(global_dir, OutDir, "web-passwords")
1267 if 'VOIP-PASSWORDS' in ExtraList:
1268 DoLink(global_dir, OutDir, "voip-passwords")
1270 if 'KEYRING' in ExtraList:
1272 bn = os.path.basename(k)
1273 if os.path.isdir(k):
1274 src = os.path.join(global_dir, bn)
1275 replaceTree(src, OutDir)
1277 DoLink(global_dir, OutDir, bn)
1281 bn = os.path.basename(k)
1282 target = os.path.join(OutDir, bn)
1283 if os.path.isdir(target):
1286 posix.remove(target)
1289 DoLink(global_dir, OutDir, "last_update.trace")
1292 def getLastLDAPChangeTime(l):
1293 mods = l.search_s('cn=log',
1294 ldap.SCOPE_ONELEVEL,
1295 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1300 # Sort the list by reqEnd
1301 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1302 # Take the last element in the array
1303 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1307 def getLastKeyringChangeTime():
1310 mt = os.path.getmtime(k)
1316 def getLastBuildTime(gdir):
1317 cache_last_ldap_mod = 0
1318 cache_last_unix_mod = 0
1321 fd = open(os.path.join(gdir, "last_update.trace"), "r")
1322 cache_last_mod=fd.read().split()
1324 cache_last_ldap_mod = cache_last_mod[0]
1325 cache_last_unix_mod = int(cache_last_mod[1])
1326 except IndexError, ValueError:
1330 if e.errno == errno.ENOENT:
1335 return (cache_last_ldap_mod, cache_last_unix_mod)
1338 parser = optparse.OptionParser()
1339 parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1340 help="Output directory.")
1341 parser.add_option("-f", "--force", dest="force", action="store_true",
1342 help="Force generation, even if not update to LDAP has happened.")
1344 (options, args) = parser.parse_args()
1349 if options.generatedir is not None:
1350 generate_dir = os.environ['UD_GENERATEDIR']
1351 elif 'UD_GENERATEDIR' in os.environ:
1352 generate_dir = os.environ['UD_GENERATEDIR']
1354 generate_dir = GenerateDir
1357 lockf = os.path.join(generate_dir, 'ud-generate.lock')
1358 lock = get_lock( lockf )
1360 sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1363 l = make_ldap_conn()
1365 time_started = int(time.time())
1366 ldap_last_mod = getLastLDAPChangeTime(l)
1367 unix_last_mod = getLastKeyringChangeTime()
1368 cache_last_ldap_mod, cache_last_unix_mod = getLastBuildTime(generate_dir)
1370 need_update = (ldap_last_mod > cache_last_ldap_mod) or (unix_last_mod > cache_last_unix_mod)
1372 if not options.force and not need_update:
1373 fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1374 fd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1378 tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1379 generate_all(generate_dir, l)
1380 tracefd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1384 if __name__ == "__main__":
1385 if 'UD_PROFILE' in os.environ:
1388 cProfile.run('ud_generate()', "udg_prof")
1389 p = pstats.Stats('udg_prof')
1390 ##p.sort_stats('time').print_stats()
1391 p.sort_stats('cumulative').print_stats()
1397 # vim:set shiftwidth=3: