3 # Generates passwd, shadow and group files from the ldap directory.
5 # Copyright (c) 2000-2001 Jason Gunthorpe <jgg@debian.org>
6 # Copyright (c) 2003-2004 James Troup <troup@debian.org>
7 # Copyright (c) 2004-2005,7 Joey Schulze <joey@infodrom.org>
8 # Copyright (c) 2001-2007 Ryan Murray <rmurray@debian.org>
9 # Copyright (c) 2008,2009,2010,2011 Peter Palfrader <peter@palfrader.org>
10 # Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
11 # Copyright (c) 2008 Mark Hymers <mhy@debian.org>
12 # Copyright (c) 2008 Luk Claes <luk@debian.org>
13 # Copyright (c) 2008 Thomas Viehmann <tv@beamnet.de>
14 # Copyright (c) 2009 Stephen Gran <steve@lobefin.net>
15 # Copyright (c) 2010 Helmut Grohne <helmut@subdivi.de>
17 # This program is free software; you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation; either version 2 of the License, or
20 # (at your option) any later version.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
31 import string, re, time, ldap, getopt, sys, os, pwd, posix, socket, base64, sha, shutil, errno, tarfile, grp
33 from userdir_ldap import *
34 from userdir_exceptions import *
37 from cStringIO import StringIO
39 from StringIO import StringIO
41 import simplejson as json
44 if not '__author__' in json.__dict__:
45 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
46 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
49 sys.stderr.write("You should probably not run ud-generate as root.\n")
63 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
65 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
66 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
67 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
68 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
69 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
70 isSSHFP = re.compile("^\s*IN\s+SSHFP")
71 DNSZone = ".debian.net"
72 Keyrings = ConfModule.sync_keyrings.split(":")
74 def safe_makedirs(dir):
78 if e.errno == errno.EEXIST:
87 if e.errno == errno.ENOENT:
92 def get_lock(fn, wait=5*60, max_age=3600*6):
95 if stat[ST_MTIME] < time.time() - max_age:
96 sys.stderr.write("Removing stale lock %s"%(fn))
98 except OSError, error:
99 if error.errno == errno.ENOENT:
104 lock = lockfile.FileLock(fn)
106 lock.acquire(timeout=wait)
107 except lockfile.LockTimeout:
114 return Str.translate(string.maketrans("\n\r\t", "$$$"))
116 def DoLink(From, To, File):
118 posix.remove(To + File)
121 posix.link(From + File, To + File)
123 def IsRetired(account):
125 Looks for accountStatus in the LDAP record and tries to
126 match it against one of the known retired statuses
129 status = account['accountStatus']
131 line = status.split()
134 if status == "inactive":
137 elif status == "memorial":
140 elif status == "retiring":
141 # We'll give them a few extra days over what we said
142 age = 6 * 31 * 24 * 60 * 60
144 return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
152 #def IsGidDebian(account):
153 # return account['gidNumber'] == 800
155 # See if this user is in the group list
156 def IsInGroup(account):
160 # See if the primary group is in the list
161 if str(account['gidNumber']) in Allowed: return True
163 # Check the host based ACL
164 if account.is_allowed_by_hostacl(CurrentHost): return True
166 # See if there are supplementary groups
167 if not 'supplementaryGid' in account: return False
170 addGroups(supgroups, account['supplementaryGid'], account['uid'])
172 if Allowed.has_key(g):
176 def Die(File, F, Fdb):
182 os.remove(File + ".tmp")
186 os.remove(File + ".tdb.tmp")
190 def Done(File, F, Fdb):
193 os.rename(File + ".tmp", File)
196 os.rename(File + ".tdb.tmp", File + ".tdb")
198 # Generate the password list
199 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
202 F = open(File + ".tdb.tmp", "w")
207 if not IsInGroup(a): continue
209 # Do not let people try to buffer overflow some busted passwd parser.
210 if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
212 userlist[a['uid']] = a['gidNumber']
213 line = "%s:%s:%d:%d:%s:%s%s:%s" % (
219 HomePrefix, a['uid'],
221 line = Sanitize(line) + "\n"
222 F.write("0%u %s" % (i, line))
223 F.write(".%s %s" % (a['uid'], line))
224 F.write("=%d %s" % (a['uidNumber'], line))
227 # Oops, something unspeakable happened.
233 # Return the list of users so we know which keys to export
236 def GenAllUsers(accounts, file):
239 OldMask = os.umask(0022)
240 f = open(file + ".tmp", "w", 0644)
245 all.append( { 'uid': a['uid'],
246 'uidNumber': a['uidNumber'],
247 'active': a.pw_active() and a.shadow_active() } )
250 # Oops, something unspeakable happened.
256 # Generate the shadow list
257 def GenShadow(accounts, File):
260 OldMask = os.umask(0077)
261 F = open(File + ".tdb.tmp", "w", 0600)
267 if not IsInGroup(a): continue
269 # If the account is locked, mark it as such in shadow
270 # See Debian Bug #308229 for why we set it to 1 instead of 0
271 if not a.pw_active(): ShadowExpire = '1'
272 elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
273 else: ShadowExpire = ''
276 values.append(a['uid'])
277 values.append(a.get_password())
278 for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
279 if key in a: values.append(a[key])
280 else: values.append('')
281 values.append(ShadowExpire)
282 line = ':'.join(values)+':'
283 line = Sanitize(line) + "\n"
284 F.write("0%u %s" % (i, line))
285 F.write(".%s %s" % (a['uid'], line))
288 # Oops, something unspeakable happened.
294 # Generate the sudo passwd file
295 def GenShadowSudo(accounts, File, untrusted):
298 OldMask = os.umask(0077)
299 F = open(File + ".tmp", "w", 0600)
304 if not IsInGroup(a): continue
306 if 'sudoPassword' in a:
307 for entry in a['sudoPassword']:
308 Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
311 uuid = Match.group(1)
312 status = Match.group(2)
313 hosts = Match.group(3)
314 cryptedpass = Match.group(4)
316 if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
318 for_all = hosts == "*"
319 for_this_host = CurrentHost in hosts.split(',')
320 if not (for_all or for_this_host):
322 # ignore * passwords for untrusted hosts, but copy host specific passwords
323 if for_all and untrusted:
326 if for_this_host: # this makes sure we take a per-host entry over the for-all entry
331 Line = "%s:%s" % (a['uid'], Pass)
332 Line = Sanitize(Line) + "\n"
333 F.write("%s" % (Line))
335 # Oops, something unspeakable happened.
341 # Generate the shadow list
342 def GenSSHShadow(global_dir, accounts):
343 # Fetch all the users
346 safe_rmtree(os.path.join(global_dir, 'userkeys'))
347 safe_makedirs(os.path.join(global_dir, 'userkeys'))
350 if not 'sshRSAAuthKey' in a: continue
354 OldMask = os.umask(0077)
355 File = os.path.join(global_dir, 'userkeys', a['uid'])
356 F = open(File + ".tmp", "w", 0600)
359 for I in a['sshRSAAuthKey']:
360 MultipleLine = "%s" % I
361 MultipleLine = Sanitize(MultipleLine) + "\n"
362 F.write(MultipleLine)
365 userfiles.append(os.path.basename(File))
367 # Oops, something unspeakable happened.
370 # As neither masterFileName nor masterFile are defined at any point
371 # this will raise a NameError.
372 Die(masterFileName, masterFile, None)
377 def GenSSHtarballs(global_dir, userlist, SSHFiles, grouprevmap, target):
378 OldMask = os.umask(0077)
379 tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
381 for f in userlist.keys():
382 if f not in SSHFiles:
384 # If we're not exporting their primary group, don't export
387 if userlist[f] in grouprevmap.keys():
388 grname = grouprevmap[userlist[f]]
391 if int(userlist[f]) <= 100:
392 # In these cases, look it up in the normal way so we
393 # deal with cases where, for instance, users are in group
394 # users as their primary group.
395 grname = grp.getgrgid(userlist[f])[0]
400 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])
403 to = tf.gettarinfo(os.path.join(global_dir, 'userkeys', f), f)
404 # These will only be used where the username doesn't
405 # exist on the target system for some reason; hence,
406 # in those cases, the safest thing is for the file to
407 # be owned by root but group nobody. This deals with
408 # the bloody obscure case where the group fails to exist
409 # whilst the user does (in which case we want to avoid
410 # ending up with a file which is owned user:root to avoid
411 # a fairly obvious attack vector)
414 # Using the username / groupname fields avoids any need
415 # to give a shit^W^W^Wcare about the UIDoffset stuff.
420 contents = file(os.path.join(global_dir, 'userkeys', f)).read()
422 for line in contents.splitlines():
423 if line.startswith("allowed_hosts=") and ' ' in line:
424 machines, line = line.split('=', 1)[1].split(' ', 1)
425 if CurrentHost not in machines.split(','):
426 continue # skip this key
429 continue # no keys for this host
430 contents = "\n".join(lines) + "\n"
431 to.size = len(contents)
432 tf.addfile(to, StringIO(contents))
435 os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
437 # add a list of groups to existing groups,
438 # including all subgroups thereof, recursively.
439 # basically this proceduces the transitive hull of the groups in
441 def addGroups(existingGroups, newGroups, uid):
442 for group in newGroups:
443 # if it's a <group>@host, split it and verify it's on the current host.
444 s = group.split('@', 1)
445 if len(s) == 2 and s[1] != CurrentHost:
449 # let's see if we handled this group already
450 if group in existingGroups:
453 if not GroupIDMap.has_key(group):
454 print "Group", group, "does not exist but", uid, "is in it"
457 existingGroups.append(group)
459 if SubGroupMap.has_key(group):
460 addGroups(existingGroups, SubGroupMap[group], uid)
462 # Generate the group list
463 def GenGroup(accounts, File):
467 F = open(File + ".tdb.tmp", "w")
469 # Generate the GroupMap
471 for x in GroupIDMap.keys():
473 GroupHasPrimaryMembers = {}
475 # Sort them into a list of groups having a set of users
477 GroupHasPrimaryMembers[ a['gidNumber'] ] = True
478 if not IsInGroup(a): continue
479 if not 'supplementaryGid' in a: continue
482 addGroups(supgroups, a['supplementaryGid'], a['uid'])
484 GroupMap[g].append(a['uid'])
486 # Output the group file.
488 for x in GroupMap.keys():
489 if GroupIDMap.has_key(x) == 0:
492 if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
495 grouprevmap[GroupIDMap[x]] = x
497 Line = "%s:x:%u:" % (x, GroupIDMap[x])
499 for I in GroupMap[x]:
500 Line = Line + ("%s%s" % (Comma, I))
502 Line = Sanitize(Line) + "\n"
503 F.write("0%u %s" % (J, Line))
504 F.write(".%s %s" % (x, Line))
505 F.write("=%u %s" % (GroupIDMap[x], Line))
508 # Oops, something unspeakable happened.
516 def CheckForward(accounts):
518 if not 'emailForward' in a: continue
523 if not IsInGroup(a): delete = True
524 # Do not allow people to try to buffer overflow busted parsers
525 elif len(a['emailForward']) > 200: delete = True
526 # Check the forwarding address
527 elif EmailCheck.match(a['emailForward']) is None: delete = True
530 a.delete_mailforward()
532 # Generate the email forwarding list
533 def GenForward(accounts, File):
536 OldMask = os.umask(0022)
537 F = open(File + ".tmp", "w", 0644)
541 if not 'emailForward' in a: continue
542 Line = "%s: %s" % (a['uid'], a['emailForward'])
543 Line = Sanitize(Line) + "\n"
546 # Oops, something unspeakable happened.
552 def GenCDB(accounts, File, key):
555 OldMask = os.umask(0022)
556 Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
559 # Write out the email address for each user
561 if not key in a: continue
564 Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
567 # Oops, something unspeakable happened.
571 if Fdb.close() != None:
572 raise "cdbmake gave an error"
574 # Generate the anon XEarth marker file
575 def GenMarkers(accounts, File):
578 F = open(File + ".tmp", "w")
580 # Write out the position for each user
582 if not ('latitude' in a and 'longitude' in a): continue
584 Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
585 Line = Sanitize(Line) + "\n"
590 # Oops, something unspeakable happened.
596 # Generate the debian-private subscription list
597 def GenPrivate(accounts, File):
600 F = open(File + ".tmp", "w")
602 # Write out the position for each user
604 if not a.is_active_user(): continue
605 if not 'privateSub' in a: continue
607 Line = "%s"%(a['privateSub'])
608 Line = Sanitize(Line) + "\n"
613 # Oops, something unspeakable happened.
619 # Generate a list of locked accounts
620 def GenDisabledAccounts(accounts, File):
623 F = open(File + ".tmp", "w")
624 disabled_accounts = []
626 # Fetch all the users
628 if a.pw_active(): continue
629 Line = "%s:%s" % (a['uid'], "Account is locked")
630 disabled_accounts.append(a)
631 F.write(Sanitize(Line) + "\n")
633 # Oops, something unspeakable happened.
638 return disabled_accounts
640 # Generate the list of local addresses that refuse all mail
641 def GenMailDisable(accounts, File):
644 F = open(File + ".tmp", "w")
647 if not 'mailDisableMessage' in a: continue
648 Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
649 Line = Sanitize(Line) + "\n"
652 # Oops, something unspeakable happened.
658 # Generate a list of uids that should have boolean affects applied
659 def GenMailBool(accounts, File, key):
662 F = open(File + ".tmp", "w")
665 if not key in a: continue
666 if not a[key] == 'TRUE': continue
667 Line = "%s"%(a['uid'])
668 Line = Sanitize(Line) + "\n"
671 # Oops, something unspeakable happened.
677 # Generate a list of hosts for RBL or whitelist purposes.
678 def GenMailList(accounts, File, key):
681 F = open(File + ".tmp", "w")
683 if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
684 else: validregex = re.compile('^[-\w.]+$')
687 if not key in a: continue
689 filtered = filter(lambda z: validregex.match(z), a[key])
690 if len(filtered) == 0: continue
691 if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
692 line = a['uid'] + ': ' + ' : '.join(filtered)
693 line = Sanitize(line) + "\n"
696 # Oops, something unspeakable happened.
702 def isRoleAccount(account):
703 return 'debianRoleAccount' in account['objectClass']
705 # Generate the DNS Zone file
706 def GenDNS(accounts, File):
709 F = open(File + ".tmp", "w")
711 # Fetch all the users
714 # Write out the zone file entry for each user
716 if not 'dnsZoneEntry' in a: continue
717 if not a.is_active_user() and not isRoleAccount(a): continue
720 F.write("; %s\n"%(a.email_address()))
721 for z in a["dnsZoneEntry"]:
722 Split = z.lower().split()
723 if Split[1].lower() == 'in':
724 for y in range(0, len(Split)):
727 Line = " ".join(Split) + "\n"
730 Host = Split[0] + DNSZone
731 if BSMTPCheck.match(Line) != None:
732 F.write("; Has BSMTP\n")
734 # Write some identification information
735 if not RRs.has_key(Host):
736 if Split[2].lower() in ["a", "aaaa"]:
737 Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
738 for y in a["keyFingerPrint"]:
739 Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
743 Line = "; Err %s"%(str(Split))
748 F.write("; Errors:\n")
749 for line in str(e).split("\n"):
750 F.write("; %s\n"%(line))
753 # Oops, something unspeakable happened.
759 def ExtractDNSInfo(x):
763 TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
766 if x[1].has_key("ipHostNumber"):
767 for I in x[1]["ipHostNumber"]:
768 if IsV6Addr.match(I) != None:
769 DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
771 DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
775 if 'sshRSAHostKey' in x[1]:
776 for I in x[1]["sshRSAHostKey"]:
778 if Split[0] == 'ssh-rsa':
780 if Split[0] == 'ssh-dss':
782 if Algorithm == None:
784 Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
785 DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
787 if 'architecture' in x[1]:
788 Arch = GetAttr(x, "architecture")
790 if x[1].has_key("machine"):
791 Mach = " " + GetAttr(x, "machine")
792 DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
794 if x[1].has_key("mXRecord"):
795 for I in x[1]["mXRecord"]:
796 DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
800 # Generate the DNS records
801 def GenZoneRecords(host_attrs, File):
804 F = open(File + ".tmp", "w")
806 # Fetch all the hosts
808 if x[1].has_key("hostname") == 0:
811 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
814 DNSInfo = ExtractDNSInfo(x)
818 Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
821 Line = "\t\t\t%s" % (Line)
825 # this would write sshfp lines for services on machines
826 # but we can't yet, since some are cnames and we'll make
827 # an invalid zonefile
829 # for i in x[1].get("purpose", []):
830 # m = PurposeHostField.match(i)
833 # # we ignore [[*..]] entries
834 # if m.startswith('*'):
836 # if m.startswith('-'):
839 # if not m.endswith(HostDomain):
841 # if not m.endswith('.'):
843 # for Line in DNSInfo:
844 # if isSSHFP.match(Line):
845 # Line = "%s\t%s" % (m, Line)
846 # F.write(Line + "\n")
848 # Oops, something unspeakable happened.
854 # Generate the BSMTP file
855 def GenBSMTP(accounts, File, HomePrefix):
858 F = open(File + ".tmp", "w")
860 # Write out the zone file entry for each user
862 if not 'dnsZoneEntry' in a: continue
863 if not a.is_active_user(): continue
866 for z in a["dnsZoneEntry"]:
867 Split = z.lower().split()
868 if Split[1].lower() == 'in':
869 for y in range(0, len(Split)):
872 Line = " ".join(Split) + "\n"
874 Host = Split[0] + DNSZone
875 if BSMTPCheck.match(Line) != None:
876 F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
877 a['uid'], HomePrefix, a['uid'], Host))
880 F.write("; Errors\n")
883 # Oops, something unspeakable happened.
889 def HostToIP(Host, mapped=True):
893 if Host[1].has_key("ipHostNumber"):
894 for addr in Host[1]["ipHostNumber"]:
895 IPAdresses.append(addr)
896 if IsV6Addr.match(addr) is None and mapped == "True":
897 IPAdresses.append("::ffff:"+addr)
901 # Generate the ssh known hosts file
902 def GenSSHKnown(host_attrs, File, mode=None):
905 OldMask = os.umask(0022)
906 F = open(File + ".tmp", "w", 0644)
910 if x[1].has_key("hostname") == 0 or \
911 x[1].has_key("sshRSAHostKey") == 0:
913 Host = GetAttr(x, "hostname")
915 if Host.endswith(HostDomain):
916 HostNames.append(Host[:-(len(HostDomain) + 1)])
918 # in the purpose field [[host|some other text]] (where some other text is optional)
919 # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
920 # file. But so that we don't have to add everything we link we can add an asterisk
921 # and say [[*... to ignore it. In order to be able to add stuff to ssh without
922 # http linking it we also support [[-hostname]] entries.
923 for i in x[1].get("purpose", []):
924 m = PurposeHostField.match(i)
927 # we ignore [[*..]] entries
928 if m.startswith('*'):
930 if m.startswith('-'):
934 if m.endswith(HostDomain):
935 HostNames.append(m[:-(len(HostDomain) + 1)])
937 for I in x[1]["sshRSAHostKey"]:
938 if mode and mode == 'authorized_keys':
940 if 'sshdistAuthKeysHost' in x[1]:
941 hosts += x[1]['sshdistAuthKeysHost']
942 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)
944 Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
945 Line = Sanitize(Line) + "\n"
947 # Oops, something unspeakable happened.
953 # Generate the debianhosts file (list of all IP addresses)
954 def GenHosts(host_attrs, File):
957 OldMask = os.umask(0022)
958 F = open(File + ".tmp", "w", 0644)
965 if IsDebianHost.match(GetAttr(x, "hostname")) is None:
968 if not 'ipHostNumber' in x[1]:
971 addrs = x[1]["ipHostNumber"]
975 addr = Sanitize(addr) + "\n"
978 # Oops, something unspeakable happened.
984 def GenKeyrings(OutDir):
986 shutil.copy(k, OutDir)
989 def get_accounts(ldap_conn):
990 # Fetch all the users
991 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0)))",\
992 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
993 "gecos", "loginShell", "userPassword", "shadowLastChange",\
994 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
995 "shadowExpire", "emailForward", "latitude", "longitude",\
996 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
997 "keyFingerPrint", "privateSub", "mailDisableMessage",\
998 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
999 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1000 "mailContentInspectionAction"])
1002 if passwd_attrs is None:
1003 raise UDEmptyList, "No Users"
1004 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1005 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1009 def get_hosts(ldap_conn):
1010 # Fetch all the hosts
1011 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1012 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1013 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1015 if HostAttrs == None:
1016 raise UDEmptyList, "No Hosts"
1018 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1023 def make_ldap_conn():
1024 # Connect to the ldap server
1026 # for testing purposes it's sometimes useful to pass username/password
1027 # via the environment
1028 if 'UD_CREDENTIALS' in os.environ:
1029 Pass = os.environ['UD_CREDENTIALS'].split()
1031 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1032 Pass = F.readline().strip().split(" ")
1034 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1038 def generate_all(global_dir, ldap_conn):
1039 accounts = get_accounts(ldap_conn)
1040 host_attrs = get_hosts(ldap_conn)
1043 # Generate global things
1044 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1046 accounts = filter(lambda x: not IsRetired(x), accounts)
1047 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1049 CheckForward(accounts)
1051 GenMailDisable(accounts, global_dir + "mail-disable")
1052 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1053 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1054 GenPrivate(accounts, global_dir + "debian-private")
1055 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1056 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1057 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1058 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1059 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1060 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1061 GenKeyrings(global_dir)
1064 GenForward(accounts, global_dir + "forward-alias")
1066 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1067 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1069 ssh_files = GenSSHShadow(global_dir, accounts)
1070 GenMarkers(accounts, global_dir + "markers")
1071 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1072 GenHosts(host_attrs, global_dir + "debianhosts")
1074 GenDNS(accounts, global_dir + "dns-zone")
1075 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1077 for host in host_attrs:
1078 if not "hostname" in host[1]:
1080 generate_host(host, global_dir, accounts, ssh_files)
1082 def generate_host(host, global_dir, accounts, ssh_files):
1085 CurrentHost = host[1]['hostname'][0]
1086 OutDir = global_dir + CurrentHost + '/'
1092 # Get the group list and convert any named groups to numerics
1094 for groupname in AllowedGroupsPreload.strip().split(" "):
1095 GroupList[groupname] = True
1096 if 'allowedGroups' in host[1]:
1097 for groupname in host[1]['allowedGroups']:
1098 GroupList[groupname] = True
1099 for groupname in GroupList.keys():
1100 if groupname in GroupIDMap:
1101 GroupList[str(GroupIDMap[groupname])] = True
1104 if 'exportOptions' in host[1]:
1105 for extra in host[1]['exportOptions']:
1106 ExtraList[extra.upper()] = True
1113 DoLink(global_dir, OutDir, "debianhosts")
1114 DoLink(global_dir, OutDir, "ssh_known_hosts")
1115 DoLink(global_dir, OutDir, "disabled-accounts")
1118 if 'NOPASSWD' in ExtraList:
1119 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1121 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1123 grouprevmap = GenGroup(accounts, OutDir + "group")
1124 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1126 # Now we know who we're allowing on the machine, export
1127 # the relevant ssh keys
1128 GenSSHtarballs(global_dir, userlist, ssh_files, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1130 if not 'NOPASSWD' in ExtraList:
1131 GenShadow(accounts, OutDir + "shadow")
1133 # Link in global things
1134 if not 'NOMARKERS' in ExtraList:
1135 DoLink(global_dir, OutDir, "markers")
1136 DoLink(global_dir, OutDir, "mail-forward.cdb")
1137 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1138 DoLink(global_dir, OutDir, "mail-disable")
1139 DoLink(global_dir, OutDir, "mail-greylist")
1140 DoLink(global_dir, OutDir, "mail-callout")
1141 DoLink(global_dir, OutDir, "mail-rbl")
1142 DoLink(global_dir, OutDir, "mail-rhsbl")
1143 DoLink(global_dir, OutDir, "mail-whitelist")
1144 DoLink(global_dir, OutDir, "all-accounts.json")
1145 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "user-forward.cdb", 'emailForward')
1146 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "batv-tokens.cdb", 'bATVToken')
1147 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1150 DoLink(global_dir, OutDir, "forward-alias")
1152 if 'DNS' in ExtraList:
1153 DoLink(global_dir, OutDir, "dns-zone")
1154 DoLink(global_dir, OutDir, "dns-sshfp")
1156 if 'AUTHKEYS' in ExtraList:
1157 DoLink(global_dir, OutDir, "authorized_keys")
1159 if 'BSMTP' in ExtraList:
1160 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1162 if 'PRIVATE' in ExtraList:
1163 DoLink(global_dir, OutDir, "debian-private")
1165 if 'KEYRING' in ExtraList:
1167 DoLink(global_dir, OutDir, os.path.basename(k))
1171 posix.remove(OutDir + os.path.basename(k))
1175 l = make_ldap_conn()
1177 # Fetch all the groups
1179 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1180 ["gid", "gidNumber", "subGroup"])
1182 # Generate the SubGroupMap and GroupIDMap
1184 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1186 if x[1].has_key("gidNumber") == 0:
1188 GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1189 if x[1].has_key("subGroup") != 0:
1190 SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1192 # override globaldir for testing
1193 if 'UD_GENERATEDIR' in os.environ:
1194 GenerateDir = os.environ['UD_GENERATEDIR']
1197 lock = get_lock( os.path.join(GenerateDir, 'ud-generate.lock') )
1199 sys.stderr.write("Could not acquire lock %s.\n"%(fn))
1202 generate_all(GenerateDir, l)
1205 if not lock is None:
1210 # vim:set shiftwidth=3: