3 # Generates passwd, shadow and group files from the ldap directory.
5 # Copyright (c) 2000-2001 Jason Gunthorpe <jgg@debian.org>
6 # Copyright (c) 2003-2004 James Troup <troup@debian.org>
7 # Copyright (c) 2004-2005,7 Joey Schulze <joey@infodrom.org>
8 # Copyright (c) 2001-2007 Ryan Murray <rmurray@debian.org>
9 # Copyright (c) 2008,2009,2010,2011 Peter Palfrader <peter@palfrader.org>
10 # Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
11 # Copyright (c) 2008 Mark Hymers <mhy@debian.org>
12 # Copyright (c) 2008 Luk Claes <luk@debian.org>
13 # Copyright (c) 2008 Thomas Viehmann <tv@beamnet.de>
14 # Copyright (c) 2009 Stephen Gran <steve@lobefin.net>
15 # Copyright (c) 2010 Helmut Grohne <helmut@subdivi.de>
17 # This program is free software; you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation; either version 2 of the License, or
20 # (at your option) any later version.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
31 import string, re, time, ldap, getopt, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp
33 from userdir_ldap import *
34 from userdir_exceptions import *
37 from cStringIO import StringIO
39 from StringIO import StringIO
41 import simplejson as json
44 if not '__author__' in json.__dict__:
45 sys.stderr.write("Warning: This is probably the wrong json module. We want python 2.6's json\n")
46 sys.stderr.write("module, or simplejson on pytyon 2.5. Let's see if/how stuff blows up.\n")
49 sys.stderr.write("You should probably not run ud-generate as root.\n")
63 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
65 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
66 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
67 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
68 IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
69 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
70 isSSHFP = re.compile("^\s*IN\s+SSHFP")
71 DNSZone = ".debian.net"
72 Keyrings = ConfModule.sync_keyrings.split(":")
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 = hashlib.new('sha1', 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 replaceTree(src, dst_basedir):
985 bn = os.path.basename(src)
986 dst = os.path.join(dst_basedir, bn)
988 shutil.copytree(src, dst)
990 def GenKeyrings(OutDir):
993 replaceTree(k, OutDir)
995 shutil.copy(k, OutDir)
998 def get_accounts(ldap_conn):
999 # Fetch all the users
1000 passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1001 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1002 "gecos", "loginShell", "userPassword", "shadowLastChange",\
1003 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1004 "shadowExpire", "emailForward", "latitude", "longitude",\
1005 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1006 "keyFingerPrint", "privateSub", "mailDisableMessage",\
1007 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1008 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1009 "mailContentInspectionAction"])
1011 if passwd_attrs is None:
1012 raise UDEmptyList, "No Users"
1013 accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1014 accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1018 def get_hosts(ldap_conn):
1019 # Fetch all the hosts
1020 HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1021 ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1022 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1024 if HostAttrs == None:
1025 raise UDEmptyList, "No Hosts"
1027 HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1032 def make_ldap_conn():
1033 # Connect to the ldap server
1035 # for testing purposes it's sometimes useful to pass username/password
1036 # via the environment
1037 if 'UD_CREDENTIALS' in os.environ:
1038 Pass = os.environ['UD_CREDENTIALS'].split()
1040 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1041 Pass = F.readline().strip().split(" ")
1043 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1047 def generate_all(global_dir, ldap_conn):
1048 accounts = get_accounts(ldap_conn)
1049 host_attrs = get_hosts(ldap_conn)
1052 # Generate global things
1053 accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1055 accounts = filter(lambda x: not IsRetired(x), accounts)
1056 #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1058 CheckForward(accounts)
1060 GenMailDisable(accounts, global_dir + "mail-disable")
1061 GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1062 GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1063 GenPrivate(accounts, global_dir + "debian-private")
1064 GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
1065 GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1066 GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1067 GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1068 GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1069 GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1070 GenKeyrings(global_dir)
1073 GenForward(accounts, global_dir + "forward-alias")
1075 GenAllUsers(accounts, global_dir + 'all-accounts.json')
1076 accounts = filter(lambda a: not a in accounts_disabled, accounts)
1078 ssh_files = GenSSHShadow(global_dir, accounts)
1079 GenMarkers(accounts, global_dir + "markers")
1080 GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1081 GenHosts(host_attrs, global_dir + "debianhosts")
1083 GenDNS(accounts, global_dir + "dns-zone")
1084 GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1086 for host in host_attrs:
1087 if not "hostname" in host[1]:
1089 generate_host(host, global_dir, accounts, ssh_files)
1091 def generate_host(host, global_dir, accounts, ssh_files):
1094 CurrentHost = host[1]['hostname'][0]
1095 OutDir = global_dir + CurrentHost + '/'
1101 # Get the group list and convert any named groups to numerics
1103 for groupname in AllowedGroupsPreload.strip().split(" "):
1104 GroupList[groupname] = True
1105 if 'allowedGroups' in host[1]:
1106 for groupname in host[1]['allowedGroups']:
1107 GroupList[groupname] = True
1108 for groupname in GroupList.keys():
1109 if groupname in GroupIDMap:
1110 GroupList[str(GroupIDMap[groupname])] = True
1113 if 'exportOptions' in host[1]:
1114 for extra in host[1]['exportOptions']:
1115 ExtraList[extra.upper()] = True
1122 DoLink(global_dir, OutDir, "debianhosts")
1123 DoLink(global_dir, OutDir, "ssh_known_hosts")
1124 DoLink(global_dir, OutDir, "disabled-accounts")
1127 if 'NOPASSWD' in ExtraList:
1128 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1130 userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1132 grouprevmap = GenGroup(accounts, OutDir + "group")
1133 GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
1135 # Now we know who we're allowing on the machine, export
1136 # the relevant ssh keys
1137 GenSSHtarballs(global_dir, userlist, ssh_files, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1139 if not 'NOPASSWD' in ExtraList:
1140 GenShadow(accounts, OutDir + "shadow")
1142 # Link in global things
1143 if not 'NOMARKERS' in ExtraList:
1144 DoLink(global_dir, OutDir, "markers")
1145 DoLink(global_dir, OutDir, "mail-forward.cdb")
1146 DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1147 DoLink(global_dir, OutDir, "mail-disable")
1148 DoLink(global_dir, OutDir, "mail-greylist")
1149 DoLink(global_dir, OutDir, "mail-callout")
1150 DoLink(global_dir, OutDir, "mail-rbl")
1151 DoLink(global_dir, OutDir, "mail-rhsbl")
1152 DoLink(global_dir, OutDir, "mail-whitelist")
1153 DoLink(global_dir, OutDir, "all-accounts.json")
1154 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "user-forward.cdb", 'emailForward')
1155 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "batv-tokens.cdb", 'bATVToken')
1156 GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1159 DoLink(global_dir, OutDir, "forward-alias")
1161 if 'DNS' in ExtraList:
1162 DoLink(global_dir, OutDir, "dns-zone")
1163 DoLink(global_dir, OutDir, "dns-sshfp")
1165 if 'AUTHKEYS' in ExtraList:
1166 DoLink(global_dir, OutDir, "authorized_keys")
1168 if 'BSMTP' in ExtraList:
1169 GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1171 if 'PRIVATE' in ExtraList:
1172 DoLink(global_dir, OutDir, "debian-private")
1174 if 'KEYRING' in ExtraList:
1176 bn = os.path.basename(k)
1177 if os.path.isdir(k):
1178 src = os.path.join(global_dir, bn)
1179 replaceTree(src, OutDir)
1181 DoLink(global_dir, OutDir, bn)
1185 bn = os.path.basename(k)
1186 target = os.path.join(OutDir, bn)
1187 if os.path.isdir(target):
1190 posix.remove(target)
1194 l = make_ldap_conn()
1196 mods = l.search_s('cn=log',
1197 ldap.SCOPE_ONELEVEL,
1198 '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1203 # Sort the list by reqEnd
1204 sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1205 # Take the last element in the array
1206 last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1208 # override globaldir for testing
1209 if 'UD_GENERATEDIR' in os.environ:
1210 GenerateDir = os.environ['UD_GENERATEDIR']
1215 fd = open(os.path.join(GenerateDir, "last_update.trace"), "r")
1216 cache_last_mod=fd.read().strip()
1219 if e.errno == errno.ENOENT:
1223 if cache_last_mod >= last:
1226 fd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
1230 # Fetch all the groups
1232 attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1233 ["gid", "gidNumber", "subGroup"])
1235 # Generate the SubGroupMap and GroupIDMap
1237 if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1239 if x[1].has_key("gidNumber") == 0:
1241 GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1242 if x[1].has_key("subGroup") != 0:
1243 SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1246 lockfile = os.path.join(GenerateDir, 'ud-generate.lock')
1247 lock = get_lock( lockfile )
1249 sys.stderr.write("Could not acquire lock %s.\n"%(lockfile))
1252 generate_all(GenerateDir, l)
1255 if not lock is None:
1260 # vim:set shiftwidth=3: