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 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, sha, shutil, errno, tarfile, grp
32 from userdir_ldap import *
33 from userdir_exceptions import *
36 from cStringIO import StringIO
38 from StringIO import StringIO
40 import simplejson as json
43 if not '__author__' in json.__dict__:
44 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
45 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
48 sys.stderr.write("You should probably not run ud-generate as root.\n")
62 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
64 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
65 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
66 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
67 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
68 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
69 isSSHFP = re.compile("^\s*IN\s+SSHFP")
70 DNSZone = ".debian.net"
71 Keyrings = ConfModule.sync_keyrings.split(":")
73 def safe_makedirs(dir):
77 if e.errno == errno.EEXIST:
86 if e.errno == errno.ENOENT:
92 return Str.translate(string.maketrans("\n\r\t", "$$$"))
94 def DoLink(From, To, File):
96 posix.remove(To + File)
99 posix.link(From + File, To + File)
101 def IsRetired(account):
103 Looks for accountStatus in the LDAP record and tries to
104 match it against one of the known retired statuses
107 status = account['accountStatus']
109 line = status.split()
112 if status == "inactive":
115 elif status == "memorial":
118 elif status == "retiring":
119 # We'll give them a few extra days over what we said
120 age = 6 * 31 * 24 * 60 * 60
122 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
130 #def IsGidDebian(account):
131 # return account['gidNumber'] == 800
133 # See if this user is in the group list
134 def IsInGroup(account):
138 # See if the primary group is in the list
139 if str(account['gidNumber']) in Allowed: return True
141 # Check the host based ACL
142 if account.is_allowed_by_hostacl(CurrentHost): return True
144 # See if there are supplementary groups
145 if not 'supplementaryGid' in account: return False
148 addGroups(supgroups, account['supplementaryGid'], account['uid'])
150 if Allowed.has_key(g):
154 def Die(File, F, Fdb):
160 os.remove(File + ".tmp")
164 os.remove(File + ".tdb.tmp")
168 def Done(File, F, Fdb):
171 os.rename(File + ".tmp", File)
174 os.rename(File + ".tdb.tmp", File + ".tdb")
176 # Generate the password list
177 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
180 F = open(File + ".tdb.tmp", "w")
185 if not IsInGroup(a): continue
187 # Do not let people try to buffer overflow some busted passwd parser.
188 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
190 userlist[a['uid']] = a['gidNumber']
191 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
197 HomePrefix, a['uid'],
199 line = Sanitize(line) + "\n"
200 F.write("0%u %s" % (i, line))
201 F.write(".%s %s" % (a['uid'], line))
202 F.write("=%d %s" % (a['uidNumber'], line))
205 # Oops, something unspeakable happened.
211 # Return the list of users so we know which keys to export
214 def GenAllUsers(accounts, file):
217 OldMask = os.umask(0022)
218 f = open(file + ".tmp", "w", 0644)
223 all.append( { 'uid': a['uid'],
224 'uidNumber': a['uidNumber'],
225 'active': a.pw_active() and a.shadow_active() } )
228 # Oops, something unspeakable happened.
234 # Generate the shadow list
235 def GenShadow(accounts, File):
238 OldMask = os.umask(0077)
239 F = open(File + ".tdb.tmp", "w", 0600)
245 if not IsInGroup(a): continue
247 # If the account is locked, mark it as such in shadow
248 # See Debian Bug #308229 for why we set it to 1 instead of 0
249 if not a.pw_active(): ShadowExpire = '1'
250 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
251 else: ShadowExpire = ''
254 values.append(a['uid'])
255 values.append(a.get_password())
256 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
257 if key in a: values.append(a[key])
258 else: values.append('')
259 values.append(ShadowExpire)
260 line = ':'.join(values)+':'
261 line = Sanitize(line) + "\n"
262 F.write("0%u %s" % (i, line))
263 F.write(".%s %s" % (a['uid'], line))
266 # Oops, something unspeakable happened.
272 # Generate the sudo passwd file
273 def GenShadowSudo(accounts, File, untrusted):
276 OldMask = os.umask(0077)
277 F = open(File + ".tmp", "w", 0600)
282 if not IsInGroup(a): continue
284 if 'sudoPassword' in a:
285 for entry in a['sudoPassword']:
286 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
289 uuid = Match.group(1)
290 status = Match.group(2)
291 hosts = Match.group(3)
292 cryptedpass = Match.group(4)
294 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
296 for_all = hosts == "*"
297 for_this_host = CurrentHost in hosts.split(',')
298 if not (for_all or for_this_host):
300 # ignore * passwords for untrusted hosts, but copy host specific passwords
301 if for_all and untrusted:
304 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
309 Line = "%s:%s" % (a['uid'], Pass)
310 Line = Sanitize(Line) + "\n"
311 F.write("%s" % (Line))
313 # Oops, something unspeakable happened.
319 # Generate the shadow list
320 def GenSSHShadow(global_dir, accounts):
321 # Fetch all the users
324 safe_rmtree(os.path.join(global_dir, 'userkeys'))
325 safe_makedirs(os.path.join(global_dir, 'userkeys'))
328 if not 'sshRSAAuthKey' in a: continue
332 OldMask = os.umask(0077)
333 File = os.path.join(global_dir, 'userkeys', a['uid'])
334 F = open(File + ".tmp", "w", 0600)
337 for I in a['sshRSAAuthKey']:
338 MultipleLine = "%s" % I
339 MultipleLine = Sanitize(MultipleLine) + "\n"
340 F.write(MultipleLine)
343 userfiles.append(os.path.basename(File))
345 # Oops, something unspeakable happened.
348 # As neither masterFileName nor masterFile are defined at any point
349 # this will raise a NameError.
350 Die(masterFileName, masterFile, None)
355 def GenSSHtarballs(global_dir, userlist, SSHFiles, grouprevmap, target):
356 OldMask = os.umask(0077)
357 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
359 for f in userlist.keys():
360 if f not in SSHFiles:
362 # If we're not exporting their primary group, don't export
365 if userlist[f] in grouprevmap.keys():
366 grname = grouprevmap[userlist[f]]
369 if int(userlist[f]) <= 100:
370 # In these cases, look it up in the normal way so we
371 # deal with cases where, for instance, users are in group
372 # users as their primary group.
373 grname = grp.getgrgid(userlist[f])[0]
378 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])
381 to = tf.gettarinfo(os.path.join(global_dir, 'userkeys', f), f)
382 # These will only be used where the username doesn't
383 # exist on the target system for some reason; hence,
384 # in those cases, the safest thing is for the file to
385 # be owned by root but group nobody. This deals with
386 # the bloody obscure case where the group fails to exist
387 # whilst the user does (in which case we want to avoid
388 # ending up with a file which is owned user:root to avoid
389 # a fairly obvious attack vector)
392 # Using the username / groupname fields avoids any need
393 # to give a shit^W^W^Wcare about the UIDoffset stuff.
398 contents = file(os.path.join(global_dir, 'userkeys', f)).read()
400 for line in contents.splitlines():
401 if line.startswith("allowed_hosts=") and ' ' in line:
402 machines, line = line.split('=', 1)[1].split(' ', 1)
403 if CurrentHost not in machines.split(','):
404 continue # skip this key
407 continue # no keys for this host
408 contents = "\n".join(lines) + "\n"
409 to.size = len(contents)
410 tf.addfile(to, StringIO(contents))
413 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
415 # add a list of groups to existing groups,
416 # including all subgroups thereof, recursively.
417 # basically this proceduces the transitive hull of the groups in
419 def addGroups(existingGroups, newGroups, uid):
420 for group in newGroups:
421 # if it's a <group>@host, split it and verify it's on the current host.
422 s = group.split('@', 1)
423 if len(s) == 2 and s[1] != CurrentHost:
427 # let's see if we handled this group already
428 if group in existingGroups:
431 if not GroupIDMap.has_key(group):
432 print "Group", group, "does not exist but", uid, "is in it"
435 existingGroups.append(group)
437 if SubGroupMap.has_key(group):
438 addGroups(existingGroups, SubGroupMap[group], uid)
440 # Generate the group list
441 def GenGroup(accounts, File):
445 F = open(File + ".tdb.tmp", "w")
447 # Generate the GroupMap
449 for x in GroupIDMap.keys():
451 GroupHasPrimaryMembers = {}
453 # Sort them into a list of groups having a set of users
455 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
456 if not IsInGroup(a): continue
457 if not 'supplementaryGid' in a: continue
460 addGroups(supgroups, a['supplementaryGid'], a['uid'])
462 GroupMap[g].append(a['uid'])
464 # Output the group file.
466 for x in GroupMap.keys():
467 if GroupIDMap.has_key(x) == 0:
470 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
473 grouprevmap[GroupIDMap[x]] = x
475 Line = "%s:x:%u:" % (x, GroupIDMap[x])
477 for I in GroupMap[x]:
478 Line = Line + ("%s%s" % (Comma, I))
480 Line = Sanitize(Line) + "\n"
481 F.write("0%u %s" % (J, Line))
482 F.write(".%s %s" % (x, Line))
483 F.write("=%u %s" % (GroupIDMap[x], Line))
486 # Oops, something unspeakable happened.
494 def CheckForward(accounts):
496 if not 'emailForward' in a: continue
501 if not IsInGroup(a): delete = True
502 # Do not allow people to try to buffer overflow busted parsers
503 elif len(a['emailForward']) > 200: delete = True
504 # Check the forwarding address
505 elif EmailCheck.match(a['emailForward']) is None: delete = True
508 a.delete_mailforward()
510 # Generate the email forwarding list
511 def GenForward(accounts, File):
514 OldMask = os.umask(0022)
515 F = open(File + ".tmp", "w", 0644)
519 if not 'emailForward' in a: continue
520 Line = "%s: %s" % (a['uid'], a['emailForward'])
521 Line = Sanitize(Line) + "\n"
524 # Oops, something unspeakable happened.
530 def GenCDB(accounts, File, key):
533 OldMask = os.umask(0022)
534 Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
537 # Write out the email address for each user
539 if not key in a: continue
542 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
545 # Oops, something unspeakable happened.
549 if Fdb.close() != None:
550 raise "cdbmake gave an error"
552 # Generate the anon XEarth marker file
553 def GenMarkers(accounts, File):
556 F = open(File + ".tmp", "w")
558 # Write out the position for each user
560 if not ('latitude' in a and 'longitude' in a): continue
562 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
563 Line = Sanitize(Line) + "\n"
568 # Oops, something unspeakable happened.
574 # Generate the debian-private subscription list
575 def GenPrivate(accounts, File):
578 F = open(File + ".tmp", "w")
580 # Write out the position for each user
582 if not a.is_active_user(): continue
583 if not 'privateSub' in a: continue
585 Line = "%s"%(a['privateSub'])
586 Line = Sanitize(Line) + "\n"
591 # Oops, something unspeakable happened.
597 # Generate a list of locked accounts
598 def GenDisabledAccounts(accounts, File):
601 F = open(File + ".tmp", "w")
602 disabled_accounts = []
604 # Fetch all the users
606 if a.pw_active(): continue
607 Line = "%s:%s" % (a['uid'], "Account is locked")
608 disabled_accounts.append(a)
609 F.write(Sanitize(Line) + "\n")
611 # Oops, something unspeakable happened.
616 return disabled_accounts
618 # Generate the list of local addresses that refuse all mail
619 def GenMailDisable(accounts, File):
622 F = open(File + ".tmp", "w")
625 if not 'mailDisableMessage' in a: continue
626 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
627 Line = Sanitize(Line) + "\n"
630 # Oops, something unspeakable happened.
636 # Generate a list of uids that should have boolean affects applied
637 def GenMailBool(accounts, File, key):
640 F = open(File + ".tmp", "w")
643 if not key in a: continue
644 if not a[key] == 'TRUE': continue
645 Line = "%s"%(a['uid'])
646 Line = Sanitize(Line) + "\n"
649 # Oops, something unspeakable happened.
655 # Generate a list of hosts for RBL or whitelist purposes.
656 def GenMailList(accounts, File, key):
659 F = open(File + ".tmp", "w")
661 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
662 else: validregex = re.compile('^[-\w.]+$')
665 if not key in a: continue
667 filtered = filter(lambda z: validregex.match(z), a[key])
668 if len(filtered) == 0: continue
669 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
670 line = a['uid'] + ': ' + ' : '.join(filtered)
671 line = Sanitize(line) + "\n"
674 # Oops, something unspeakable happened.
680 def isRoleAccount(account):
681 return 'debianRoleAccount' in account['objectClass']
683 # Generate the DNS Zone file
684 def GenDNS(accounts, File):
687 F = open(File + ".tmp", "w")
689 # Fetch all the users
692 # Write out the zone file entry for each user
694 if not 'dnsZoneEntry' in a: continue
695 if not a.is_active_user() and not isRoleAccount(a): continue
698 F.write("; %s\n"%(a.email_address()))
699 for z in a["dnsZoneEntry"]:
700 Split = z.lower().split()
701 if Split[1].lower() == 'in':
702 for y in range(0, len(Split)):
705 Line = " ".join(Split) + "\n"
708 Host = Split[0] + DNSZone
709 if BSMTPCheck.match(Line) != None:
710 F.write("; Has BSMTP\n")
712 # Write some identification information
713 if not RRs.has_key(Host):
714 if Split[2].lower() in ["a", "aaaa"]:
715 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
716 for y in a["keyFingerPrint"]:
717 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
721 Line = "; Err %s"%(str(Split))
726 F.write("; Errors:\n")
727 for line in str(e).split("\n"):
728 F.write("; %s\n"%(line))
731 # Oops, something unspeakable happened.
737 def ExtractDNSInfo(x):
741 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
744 if x[1].has_key("ipHostNumber"):
745 for I in x[1]["ipHostNumber"]:
746 if IsV6Addr.match(I) != None:
747 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
749 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
753 if 'sshRSAHostKey' in x[1]:
754 for I in x[1]["sshRSAHostKey"]:
756 if Split[0] == 'ssh-rsa':
758 if Split[0] == 'ssh-dss':
760 if Algorithm == None:
762 Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
763 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
765 if 'architecture' in x[1]:
766 Arch = GetAttr(x, "architecture")
768 if x[1].has_key("machine"):
769 Mach = " " + GetAttr(x, "machine")
770 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
772 if x[1].has_key("mXRecord"):
773 for I in x[1]["mXRecord"]:
774 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
778 # Generate the DNS records
779 def GenZoneRecords(host_attrs, File):
782 F = open(File + ".tmp", "w")
784 # Fetch all the hosts
786 if x[1].has_key("hostname") == 0:
789 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
792 DNSInfo = ExtractDNSInfo(x)
796 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
799 Line = "\t\t\t%s" % (Line)
803 # this would write sshfp lines for services on machines
804 # but we can't yet, since some are cnames and we'll make
805 # an invalid zonefile
807 # for i in x[1].get("purpose", []):
808 # m = PurposeHostField.match(i)
811 # # we ignore [[*..]] entries
812 # if m.startswith('*'):
814 # if m.startswith('-'):
817 # if not m.endswith(HostDomain):
819 # if not m.endswith('.'):
821 # for Line in DNSInfo:
822 # if isSSHFP.match(Line):
823 # Line = "%s\t%s" % (m, Line)
824 # F.write(Line + "\n")
826 # Oops, something unspeakable happened.
832 # Generate the BSMTP file
833 def GenBSMTP(accounts, File, HomePrefix):
836 F = open(File + ".tmp", "w")
838 # Write out the zone file entry for each user
840 if not 'dnsZoneEntry' in a: continue
841 if not a.is_active_user(): continue
844 for z in a["dnsZoneEntry"]:
845 Split = z.lower().split()
846 if Split[1].lower() == 'in':
847 for y in range(0, len(Split)):
850 Line = " ".join(Split) + "\n"
852 Host = Split[0] + DNSZone
853 if BSMTPCheck.match(Line) != None:
854 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
855 a['uid'], HomePrefix, a['uid'], Host))
858 F.write("; Errors\n")
861 # Oops, something unspeakable happened.
867 def HostToIP(Host, mapped=True):
871 if Host[1].has_key("ipHostNumber"):
872 for addr in Host[1]["ipHostNumber"]:
873 IPAdresses.append(addr)
874 if IsV6Addr.match(addr) is None and mapped == "True":
875 IPAdresses.append("::ffff:"+addr)
879 # Generate the ssh known hosts file
880 def GenSSHKnown(host_attrs, File, mode=None):
883 OldMask = os.umask(0022)
884 F = open(File + ".tmp", "w", 0644)
888 if x[1].has_key("hostname") == 0 or \
889 x[1].has_key("sshRSAHostKey") == 0:
891 Host = GetAttr(x, "hostname")
893 if Host.endswith(HostDomain):
894 HostNames.append(Host[:-(len(HostDomain) + 1)])
896 # in the purpose field [[host|some other text]] (where some other text is optional)
897 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
898 # file. But so that we don't have to add everything we link we can add an asterisk
899 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
900 # http linking it we also support [[-hostname]] entries.
901 for i in x[1].get("purpose", []):
902 m = PurposeHostField.match(i)
905 # we ignore [[*..]] entries
906 if m.startswith('*'):
908 if m.startswith('-'):
912 if m.endswith(HostDomain):
913 HostNames.append(m[:-(len(HostDomain) + 1)])
915 for I in x[1]["sshRSAHostKey"]:
916 if mode and mode == 'authorized_keys':
918 if 'sshdistAuthKeysHost' in x[1]:
919 hosts += x[1]['sshdistAuthKeysHost']
920 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)
922 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
923 Line = Sanitize(Line) + "\n"
925 # Oops, something unspeakable happened.
931 # Generate the debianhosts file (list of all IP addresses)
932 def GenHosts(host_attrs, File):
935 OldMask = os.umask(0022)
936 F = open(File + ".tmp", "w", 0644)
943 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
946 if not 'ipHostNumber' in x[1]:
949 addrs = x[1]["ipHostNumber"]
953 addr = Sanitize(addr) + "\n"
956 # Oops, something unspeakable happened.
962 def GenKeyrings(OutDir):
964 shutil.copy(k, OutDir)
967 def get_accounts(ldap_conn):
968 # Fetch all the users
969 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0)))",\
970 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
971 "gecos", "loginShell", "userPassword", "shadowLastChange",\
972 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
973 "shadowExpire", "emailForward", "latitude", "longitude",\
974 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
975 "keyFingerPrint", "privateSub", "mailDisableMessage",\
976 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
977 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
978 "mailContentInspectionAction"])
980 if passwd_attrs is None:
981 raise UDEmptyList, "No Users"
982 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
983 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
987 def get_hosts(ldap_conn):
988 # Fetch all the hosts
989 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
990 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
991 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
993 if HostAttrs == None:
994 raise UDEmptyList, "No Hosts"
996 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1001 def make_ldap_conn():
1002 # Connect to the ldap server
1004 # for testing purposes it's sometimes useful to pass username/password
1005 # via the environment
1006 if 'UD_CREDENTIALS' in os.environ:
1007 Pass = os.environ['UD_CREDENTIALS'].split()
1009 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1010 Pass = F.readline().strip().split(" ")
1012 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1016 def generate_all(global_dir, ldap_conn):
1017 accounts = get_accounts(ldap_conn)
1018 host_attrs = get_hosts(ldap_conn)
1021 # Generate global things
1022 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1024 accounts = filter(lambda x: not IsRetired(x), accounts)
1025 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1027 CheckForward(accounts)
1029 GenMailDisable(accounts, global_dir + "mail-disable")
1030 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1031 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1032 GenPrivate(accounts, global_dir + "debian-private")
1033 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1034 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1035 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1036 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1037 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1038 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1039 GenKeyrings(global_dir)
1042 GenForward(accounts, global_dir + "forward-alias")
1044 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1045 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1047 ssh_files = GenSSHShadow(global_dir, accounts)
1048 GenMarkers(accounts, global_dir + "markers")
1049 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1050 GenHosts(host_attrs, global_dir + "debianhosts")
1052 GenDNS(accounts, global_dir + "dns-zone")
1053 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1055 for host in host_attrs:
1056 if not "hostname" in host[1]:
1058 generate_host(host, global_dir, accounts, ssh_files)
1060 def generate_host(host, global_dir, accounts, ssh_files):
1063 CurrentHost = host[1]['hostname'][0]
1064 OutDir = global_dir + CurrentHost + '/'
1070 # Get the group list and convert any named groups to numerics
1072 for groupname in AllowedGroupsPreload.strip().split(" "):
1073 GroupList[groupname] = True
1074 if 'allowedGroups' in host[1]:
1075 for groupname in host[1]['allowedGroups']:
1076 GroupList[groupname] = True
1077 for groupname in GroupList.keys():
1078 if groupname in GroupIDMap:
1079 GroupList[str(GroupIDMap[groupname])] = True
1082 if 'exportOptions' in host[1]:
1083 for extra in host[1]['exportOptions']:
1084 ExtraList[extra.upper()] = True
1091 DoLink(global_dir, OutDir, "debianhosts")
1092 DoLink(global_dir, OutDir, "ssh_known_hosts")
1093 DoLink(global_dir, OutDir, "disabled-accounts")
1096 if 'NOPASSWD' in ExtraList:
1097 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1099 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1101 grouprevmap = GenGroup(accounts, OutDir + "group")
1102 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1104 # Now we know who we're allowing on the machine, export
1105 # the relevant ssh keys
1106 GenSSHtarballs(global_dir, userlist, ssh_files, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1108 if not 'NOPASSWD' in ExtraList:
1109 GenShadow(accounts, OutDir + "shadow")
1111 # Link in global things
1112 if not 'NOMARKERS' in ExtraList:
1113 DoLink(global_dir, OutDir, "markers")
1114 DoLink(global_dir, OutDir, "mail-forward.cdb")
1115 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1116 DoLink(global_dir, OutDir, "mail-disable")
1117 DoLink(global_dir, OutDir, "mail-greylist")
1118 DoLink(global_dir, OutDir, "mail-callout")
1119 DoLink(global_dir, OutDir, "mail-rbl")
1120 DoLink(global_dir, OutDir, "mail-rhsbl")
1121 DoLink(global_dir, OutDir, "mail-whitelist")
1122 DoLink(global_dir, OutDir, "all-accounts.json")
1123 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "user-forward.cdb", 'emailForward')
1124 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "batv-tokens.cdb", 'bATVToken')
1125 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1128 DoLink(global_dir, OutDir, "forward-alias")
1130 if 'DNS' in ExtraList:
1131 DoLink(global_dir, OutDir, "dns-zone")
1132 DoLink(global_dir, OutDir, "dns-sshfp")
1134 if 'AUTHKEYS' in ExtraList:
1135 DoLink(global_dir, OutDir, "authorized_keys")
1137 if 'BSMTP' in ExtraList:
1138 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1140 if 'PRIVATE' in ExtraList:
1141 DoLink(global_dir, OutDir, "debian-private")
1143 if 'KEYRING' in ExtraList:
1145 DoLink(global_dir, OutDir, os.path.basename(k))
1149 posix.remove(OutDir + os.path.basename(k))
1153 l = make_ldap_conn()
1155 # Fetch all the groups
1157 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1158 ["gid", "gidNumber", "subGroup"])
1160 # Generate the SubGroupMap and GroupIDMap
1162 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1164 if x[1].has_key("gidNumber") == 0:
1166 GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1167 if x[1].has_key("subGroup") != 0:
1168 SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1170 # override globaldir for testing
1171 if 'UD_GENERATEDIR' in os.environ:
1172 GenerateDir = os.environ['UD_GENERATEDIR']
1174 generate_all(GenerateDir, l)
1178 # vim:set shiftwidth=3: