+ # Oops, something unspeakable happened.
+ except:
+ Die(File, F, None)
+ raise
+ Done(File, F, None)
+
+# Generate the debian-private subscription list
+def GenPrivate(accounts, File):
+ F = None
+ try:
+ F = open(File + ".tmp", "w")
+
+ # Write out the position for each user
+ for a in accounts:
+ if not a.is_active_user(): continue
+ if a.is_guest_account(): continue
+ if not 'privateSub' in a: continue
+ try:
+ Line = "%s"%(a['privateSub'])
+ Line = Sanitize(Line) + "\n"
+ F.write(Line)
+ except:
+ pass
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(File, F, None)
+ raise
+ Done(File, F, None)
+
+# Generate a list of locked accounts
+def GenDisabledAccounts(accounts, File):
+ F = None
+ try:
+ F = open(File + ".tmp", "w")
+ disabled_accounts = []
+
+ # Fetch all the users
+ for a in accounts:
+ if a.pw_active(): continue
+ Line = "%s:%s" % (a['uid'], "Account is locked")
+ disabled_accounts.append(a)
+ F.write(Sanitize(Line) + "\n")
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(File, F, None)
+ raise
+ Done(File, F, None)
+ return disabled_accounts
+
+# Generate the list of local addresses that refuse all mail
+def GenMailDisable(accounts, File):
+ F = None
+ try:
+ F = open(File + ".tmp", "w")
+
+ for a in accounts:
+ if not 'mailDisableMessage' in a: continue
+ Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
+ Line = Sanitize(Line) + "\n"
+ F.write(Line)
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(File, F, None)
+ raise
+ Done(File, F, None)
+
+# Generate a list of uids that should have boolean affects applied
+def GenMailBool(accounts, File, key):
+ F = None
+ try:
+ F = open(File + ".tmp", "w")
+
+ for a in accounts:
+ if not key in a: continue
+ if not a[key] == 'TRUE': continue
+ Line = "%s"%(a['uid'])
+ Line = Sanitize(Line) + "\n"
+ F.write(Line)
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(File, F, None)
+ raise
+ Done(File, F, None)
+
+# Generate a list of hosts for RBL or whitelist purposes.
+def GenMailList(accounts, File, key):
+ F = None
+ try:
+ F = open(File + ".tmp", "w")
+
+ if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
+ else: validregex = re.compile('^[-\w.]+$')
+
+ for a in accounts:
+ if not key in a: continue
+
+ filtered = filter(lambda z: validregex.match(z), a[key])
+ if len(filtered) == 0: continue
+ if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
+ line = a['uid'] + ': ' + ' : '.join(filtered)
+ line = Sanitize(line) + "\n"
+ F.write(line)
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(File, F, None)
+ raise
+ Done(File, F, None)
+
+def isRoleAccount(account):
+ return 'debianRoleAccount' in account['objectClass']
+
+# Generate the DNS Zone file
+def GenDNS(accounts, File):
+ F = None
+ try:
+ F = open(File + ".tmp", "w")
+
+ # Fetch all the users
+ RRs = {}
+
+ # Write out the zone file entry for each user
+ for a in accounts:
+ if not 'dnsZoneEntry' in a: continue
+ if not a.is_active_user() and not isRoleAccount(a): continue
+ if a.is_guest_account(): continue
+
+ try:
+ F.write("; %s\n"%(a.email_address()))
+ for z in a["dnsZoneEntry"]:
+ Split = z.lower().split()
+ if Split[1].lower() == 'in':
+ Line = " ".join(Split) + "\n"
+ F.write(Line)
+
+ Host = Split[0] + DNSZone
+ if BSMTPCheck.match(Line) != None:
+ F.write("; Has BSMTP\n")
+
+ # Write some identification information
+ if not RRs.has_key(Host):
+ if Split[2].lower() in ["a", "aaaa"]:
+ Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
+ for y in a["keyFingerPrint"]:
+ Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
+ F.write(Line)
+ RRs[Host] = 1
+ else:
+ Line = "; Err %s"%(str(Split))
+ F.write(Line)
+
+ F.write("\n")
+ except Exception, e:
+ F.write("; Errors:\n")
+ for line in str(e).split("\n"):
+ F.write("; %s\n"%(line))
+ pass
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(File, F, None)
+ raise
+ Done(File, F, None)
+
+def is_ipv6_addr(i):
+ try:
+ socket.inet_pton(socket.AF_INET6, i)
+ except socket.error:
+ return False
+ return True
+
+def ExtractDNSInfo(x):
+ hostname = GetAttr(x, "hostname")
+
+ TTLprefix="\t"
+ if 'dnsTTL' in x[1]:
+ TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
+
+ DNSInfo = []
+ if x[1].has_key("ipHostNumber"):
+ for I in x[1]["ipHostNumber"]:
+ if is_ipv6_addr(I):
+ DNSInfo.append("%s.\t%sIN\tAAAA\t%s" % (hostname, TTLprefix, I))
+ else:
+ DNSInfo.append("%s.\t%sIN\tA\t%s" % (hostname, TTLprefix, I))
+
+ Algorithm = None
+
+ ssh_hostnames = [ hostname ]
+ if x[1].has_key("sshfpHostname"):
+ ssh_hostnames += [ h for h in x[1]["sshfpHostname"] ]
+
+ if 'sshRSAHostKey' in x[1]:
+ for I in x[1]["sshRSAHostKey"]:
+ Split = I.split()
+ key_prefix = Split[0]
+ key = base64.decodestring(Split[1])
+
+ # RFC4255
+ # https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml
+ if key_prefix == 'ssh-rsa':
+ Algorithm = 1
+ if key_prefix == 'ssh-dss':
+ Algorithm = 2
+ if key_prefix == 'ssh-ed25519':
+ Algorithm = 4
+ if Algorithm == None:
+ continue
+ # and more from the registry
+ sshfp_digest_codepoints = [ (1, 'sha1'), (2, 'sha256') ]
+
+ fingerprints = [ ( digest_codepoint, hashlib.new(algorithm, key).hexdigest() ) for digest_codepoint, algorithm in sshfp_digest_codepoints ]
+ for h in ssh_hostnames:
+ for digest_codepoint, fingerprint in fingerprints:
+ DNSInfo.append("%s.\t%sIN\tSSHFP\t%u %d %s" % (h, TTLprefix, Algorithm, digest_codepoint, fingerprint))
+
+ if 'architecture' in x[1]:
+ Arch = GetAttr(x, "architecture")
+ Mach = ""
+ if x[1].has_key("machine"):
+ Mach = " " + GetAttr(x, "machine")
+ DNSInfo.append("%s.\t%sIN\tHINFO\t\"%s%s\" \"%s\"" % (hostname, TTLprefix, Arch, Mach, "Debian"))
+
+ if x[1].has_key("mXRecord"):
+ for I in x[1]["mXRecord"]:
+ if I in MX_remap:
+ for e in MX_remap[I]:
+ DNSInfo.append("%s.\t%sIN\tMX\t%s" % (hostname, TTLprefix, e))
+ else:
+ DNSInfo.append("%s.\t%sIN\tMX\t%s" % (hostname, TTLprefix, I))
+
+ return DNSInfo
+
+# Generate the DNS records
+def GenZoneRecords(host_attrs, File):
+ F = None
+ try:
+ F = open(File + ".tmp", "w")
+
+ # Fetch all the hosts
+ for x in host_attrs:
+ if x[1].has_key("hostname") == 0:
+ continue
+
+ if IsDebianHost.match(GetAttr(x, "hostname")) is None:
+ continue
+
+ for Line in ExtractDNSInfo(x):
+ F.write(Line + "\n")
+
+ # this would write sshfp lines for services on machines
+ # but we can't yet, since some are cnames and we'll make
+ # an invalid zonefile
+ #
+ # for i in x[1].get("purpose", []):
+ # m = PurposeHostField.match(i)
+ # if m:
+ # m = m.group(1)
+ # # we ignore [[*..]] entries
+ # if m.startswith('*'):
+ # continue
+ # if m.startswith('-'):
+ # m = m[1:]
+ # if m:
+ # if not m.endswith(HostDomain):
+ # continue
+ # if not m.endswith('.'):
+ # m = m + "."
+ # for Line in DNSInfo:
+ # if isSSHFP.match(Line):
+ # Line = "%s\t%s" % (m, Line)
+ # F.write(Line + "\n")
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(File, F, None)
+ raise
+ Done(File, F, None)
+
+# Generate the BSMTP file
+def GenBSMTP(accounts, File, HomePrefix):
+ F = None
+ try:
+ F = open(File + ".tmp", "w")
+
+ # Write out the zone file entry for each user
+ for a in accounts:
+ if not 'dnsZoneEntry' in a: continue
+ if not a.is_active_user(): continue
+
+ try:
+ for z in a["dnsZoneEntry"]:
+ Split = z.lower().split()
+ if Split[1].lower() == 'in':
+ for y in range(0, len(Split)):
+ if Split[y] == "$":
+ Split[y] = "\n\t"
+ Line = " ".join(Split) + "\n"
+
+ Host = Split[0] + DNSZone
+ if BSMTPCheck.match(Line) != None:
+ F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
+ a['uid'], HomePrefix, a['uid'], Host))
+
+ except:
+ F.write("; Errors\n")
+ pass
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(File, F, None)
+ raise
+ Done(File, F, None)
+
+def HostToIP(Host, mapped=True):
+
+ IPAdresses = []
+
+ if Host[1].has_key("ipHostNumber"):
+ for addr in Host[1]["ipHostNumber"]:
+ IPAdresses.append(addr)
+ if not is_ipv6_addr(addr) and mapped == "True":
+ IPAdresses.append("::ffff:"+addr)
+
+ return IPAdresses
+
+# Generate the ssh known hosts file
+def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
+ F = None
+ try:
+ OldMask = os.umask(0022)
+ F = open(File + ".tmp", "w", 0644)
+ os.umask(OldMask)
+
+ for x in host_attrs:
+ if x[1].has_key("hostname") == 0 or \
+ x[1].has_key("sshRSAHostKey") == 0:
+ continue
+ Host = GetAttr(x, "hostname")
+ HostNames = [ Host ]
+ if Host.endswith(HostDomain):
+ HostNames.append(Host[:-(len(HostDomain) + 1)])
+
+ # in the purpose field [[host|some other text]] (where some other text is optional)
+ # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
+ # file. But so that we don't have to add everything we link we can add an asterisk
+ # and say [[*... to ignore it. In order to be able to add stuff to ssh without
+ # http linking it we also support [[-hostname]] entries.
+ for i in x[1].get("purpose", []):
+ m = PurposeHostField.match(i)
+ if m:
+ m = m.group(1)
+ # we ignore [[*..]] entries
+ if m.startswith('*'):
+ continue
+ if m.startswith('-'):
+ m = m[1:]
+ if m:
+ HostNames.append(m)
+ if m.endswith(HostDomain):
+ HostNames.append(m[:-(len(HostDomain) + 1)])
+
+ for I in x[1]["sshRSAHostKey"]:
+ if mode and mode == 'authorized_keys':
+ hosts = HostToIP(x)
+ if 'sshdistAuthKeysHost' in x[1]:
+ hosts += x[1]['sshdistAuthKeysHost']
+ clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
+ clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
+ Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
+ else:
+ Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
+ Line = Sanitize(Line) + "\n"
+ F.write(Line)
+ # Oops, something unspeakable happened.
+ except:
+ Die(File, F, None)
+ raise
+ Done(File, F, None)
+
+# Generate the debianhosts file (list of all IP addresses)
+def GenHosts(host_attrs, File):
+ F = None
+ try:
+ OldMask = os.umask(0022)
+ F = open(File + ".tmp", "w", 0644)
+ os.umask(OldMask)
+
+ seen = set()
+
+ for x in host_attrs:
+
+ if IsDebianHost.match(GetAttr(x, "hostname")) is None:
+ continue
+
+ if not 'ipHostNumber' in x[1]:
+ continue
+
+ addrs = x[1]["ipHostNumber"]
+ for addr in addrs:
+ if addr not in seen:
+ seen.add(addr)
+ addr = Sanitize(addr) + "\n"
+ F.write(addr)
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(File, F, None)
+ raise
+ Done(File, F, None)
+
+def replaceTree(src, dst_basedir):
+ bn = os.path.basename(src)
+ dst = os.path.join(dst_basedir, bn)
+ safe_rmtree(dst)
+ shutil.copytree(src, dst)
+
+def GenKeyrings(OutDir):
+ for k in Keyrings:
+ if os.path.isdir(k):
+ replaceTree(k, OutDir)
+ else:
+ shutil.copy(k, OutDir)
+
+
+def get_accounts(ldap_conn):
+ # Fetch all the users
+ passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
+ ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
+ "gecos", "loginShell", "userPassword", "shadowLastChange",\
+ "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
+ "shadowExpire", "emailForward", "latitude", "longitude",\
+ "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
+ "keyFingerPrint", "privateSub", "mailDisableMessage",\
+ "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
+ "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
+ "mailContentInspectionAction", "webPassword", "rtcPassword",\
+ "bATVToken", "totpSeed"])
+
+ if passwd_attrs is None:
+ raise UDEmptyList, "No Users"
+ accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
+ accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
+
+ return accounts
+
+def get_hosts(ldap_conn):
+ # Fetch all the hosts
+ HostAttrs = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
+ ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
+ "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture",
+ "sshfpHostname"])
+
+ if HostAttrs == None:
+ raise UDEmptyList, "No Hosts"
+
+ HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
+
+ return HostAttrs
+
+
+def make_ldap_conn():
+ # Connect to the ldap server
+ l = connectLDAP()
+ # for testing purposes it's sometimes useful to pass username/password
+ # via the environment
+ if 'UD_CREDENTIALS' in os.environ:
+ Pass = os.environ['UD_CREDENTIALS'].split()
+ else:
+ F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
+ Pass = F.readline().strip().split(" ")
+ F.close()
+ l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
+
+ return l
+
+
+
+def setup_group_maps(l):
+ # Fetch all the groups
+ group_id_map = {}
+ subgroup_map = {}
+ attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
+ ["gid", "gidNumber", "subGroup"])
+
+ # Generate the subgroup_map and group_id_map
+ for x in attrs:
+ if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
+ continue
+ if x[1].has_key("gidNumber") == 0:
+ continue
+ group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
+ if x[1].has_key("subGroup") != 0:
+ subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
+
+ global SubGroupMap
+ global GroupIDMap
+ SubGroupMap = subgroup_map
+ GroupIDMap = group_id_map
+
+def generate_all(global_dir, ldap_conn):
+ accounts = get_accounts(ldap_conn)
+ host_attrs = get_hosts(ldap_conn)
+
+ global_dir += '/'
+ # Generate global things
+ accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
+
+ accounts = filter(lambda x: not IsRetired(x), accounts)
+
+ CheckForward(accounts)
+
+ GenMailDisable(accounts, global_dir + "mail-disable")
+ GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
+ GenDBM(accounts, global_dir + "mail-forward.db", 'emailForward')
+ GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
+ GenDBM(accounts, global_dir + "mail-contentinspectionaction.db", 'mailContentInspectionAction')
+ GenPrivate(accounts, global_dir + "debian-private")
+ GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
+ GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
+ GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
+ GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
+ GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
+ GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
+ GenWebPassword(accounts, global_dir + "web-passwords")
+ GenRtcPassword(accounts, global_dir + "rtc-passwords")
+ GenTOTPSeed(accounts, global_dir + "users.oath")
+ GenKeyrings(global_dir)
+
+ # Compatibility.
+ GenForward(accounts, global_dir + "forward-alias")
+
+ GenAllUsers(accounts, global_dir + 'all-accounts.json')
+ accounts = filter(lambda a: not a in accounts_disabled, accounts)
+
+ ssh_userkeys = GenSSHShadow(global_dir, accounts)
+ GenMarkers(accounts, global_dir + "markers")
+ GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
+ GenHosts(host_attrs, global_dir + "debianhosts")
+
+ GenDNS(accounts, global_dir + "dns-zone")
+ GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
+
+ setup_group_maps(ldap_conn)
+
+ for host in host_attrs:
+ if not "hostname" in host[1]:
+ continue
+ generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
+
+def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
+ current_host = host[1]['hostname'][0]
+ OutDir = global_dir + current_host + '/'
+ if not os.path.isdir(OutDir):
+ os.mkdir(OutDir)