/*
* InspIRCd -- Internet Relay Chat Daemon
*
* Copyright (C) 2020 Joel Sing <joel@sing.id.au>
* Copyright (C) 2019-2023, 2026 Sadie Powell <sadie@sadiepowell.dev>
* Copyright (C) 2014-2015 Attila Molnar <attilamolnar@hush.com>
* Copyright (C) 2014 Thiago Crepaldi <thiago@thiagocrepaldi.com>
* Copyright (C) 2013-2014, 2017 Adam <Adam@anope.org>
*
* This file is part of InspIRCd. InspIRCd is free software: you can
* redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, version 2.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "inspircd.h"
#include "extension.h"
#include "modules/ldap.h"
namespace
{
Module* me;
std::string killreason;
BoolExtItem* authed;
bool verbose;
std::string vhost;
StringExtItem* vhosts;
std::vector<std::pair<std::string, std::string>> requiredattributes;
}
class BindInterface final
: public LDAPInterface
{
const std::string provider;
const std::string uid;
std::string DN;
bool checkingAttributes = false;
bool passed = false;
int attrCount = 0;
static std::string SafeReplace(const std::string& text, std::map<std::string, std::string>& replacements)
{
std::string result;
result.reserve(text.length());
for (unsigned int i = 0; i < text.length(); ++i)
{
char c = text[i];
if (c == '$')
{
// find the first nonalpha
i++;
unsigned int start = i;
while (i < text.length() - 1 && isalpha(text[i + 1]))
++i;
std::string key(text, start, (i - start) + 1);
result.append(replacements[key]);
}
else
result.push_back(c);
}
return result;
}
static void SetVHost(User* user, const std::string& DN)
{
if (!vhost.empty())
{
irc::commasepstream stream(DN);
// mashed map of key:value parts of the DN
std::map<std::string, std::string> dnParts;
std::string dnPart;
while (stream.GetToken(dnPart))
{
std::string::size_type pos = dnPart.find('=');
if (pos == std::string::npos) // malformed
continue;
std::string key(dnPart, 0, pos);
std::string value(dnPart, pos + 1, dnPart.length() - pos + 1); // +1s to skip the = itself
dnParts[key] = value;
}
// change host according to config key
vhosts->Set(user, SafeReplace(vhost, dnParts));
}
}
public:
BindInterface(Module* c, const std::string& p, const std::string& u, const std::string& dn)
: LDAPInterface(c)
, provider(p)
, uid(u)
, DN(dn)
{
}
void OnResult(const LDAPResult& r) override
{
auto* user = ServerInstance->Users.FindUUID(uid);
dynamic_reference<LDAPProvider> LDAP(me, provider);
if (!user || !LDAP)
{
if (!checkingAttributes || !--attrCount)
delete this;
return;
}
if (!checkingAttributes && requiredattributes.empty())
{
if (verbose)
ServerInstance->SNO.WriteToSnoMask('c', "Successful connection from {} (dn={})", user->GetRealMask(), DN);
// We're done, there are no attributes to check
SetVHost(user, DN);
authed->Set(user);
delete this;
return;
}
// Already checked attributes?
if (checkingAttributes)
{
if (!passed)
{
// Only one has to pass
passed = true;
if (verbose)
ServerInstance->SNO.WriteToSnoMask('c', "Successful connection from {} (dn={})", user->GetRealMask(), DN);
SetVHost(user, DN);
authed->Set(user);
}
// Delete this if this is the last ref
if (!--attrCount)
delete this;
return;
}
// check required attributes
checkingAttributes = true;
for (const auto& [attr, val] : requiredattributes)
{
// Note that only one of these has to match for it to be success
ServerInstance->Logs.Debug(MODNAME, "LDAP compare: {}={}", attr, val);
try
{
LDAP->Compare(this, DN, attr, val);
++attrCount;
}
catch (const LDAPException& ex)
{
if (verbose)
ServerInstance->SNO.WriteToSnoMask('c', "Unable to compare attributes {}={}: {}", attr, val, ex.GetReason());
}
}
// Nothing done
if (!attrCount)
{
if (verbose)
{
ServerInstance->SNO.WriteToSnoMask('c', "Forbidden connection from {} (dn={}) (unable to validate attributes)",
user->GetRealMask(), DN);
}
ServerInstance->Users.QuitUser(user, killreason);
delete this;
}
}
void OnError(const LDAPResult& err) override
{
if (checkingAttributes && --attrCount)
return;
if (passed)
{
delete this;
return;
}
auto* user = ServerInstance->Users.FindUUID(uid);
if (user)
{
if (verbose)
{
ServerInstance->SNO.WriteToSnoMask('c', "Forbidden connection from {} ({})",
user->GetRealMask(), err.getError());
}
ServerInstance->Users.QuitUser(user, killreason);
}
delete this;
}
};
class SearchInterface final
: public LDAPInterface
{
const std::string provider;
const std::string uid;
public:
SearchInterface(Module* c, const std::string& p, const std::string& u)
: LDAPInterface(c)
, provider(p)
, uid(u)
{
}
void OnResult(const LDAPResult& r) override
{
LocalUser* user = ServerInstance->Users.FindUUID<LocalUser>(uid);
dynamic_reference<LDAPProvider> LDAP(me, provider);
if (!LDAP || r.empty() || !user)
{
if (user)
ServerInstance->Users.QuitUser(user, killreason);
delete this;
return;
}
try
{
const LDAPAttributes& a = r.get(0);
std::string bindDn = a.get("dn");
if (bindDn.empty())
{
ServerInstance->Users.QuitUser(user, killreason);
delete this;
return;
}
LDAP->Bind(new BindInterface(this->creator, provider, uid, bindDn), bindDn, user->password);
}
catch (const LDAPException& ex)
{
ServerInstance->SNO.WriteToSnoMask('a', "Error searching LDAP server: " + ex.GetReason());
}
delete this;
}
void OnError(const LDAPResult& err) override
{
ServerInstance->SNO.WriteToSnoMask('a', "Error searching LDAP server: {}", err.getError());
auto* user = ServerInstance->Users.FindUUID(uid);
if (user)
ServerInstance->Users.QuitUser(user, killreason);
delete this;
}
};
class AdminBindInterface final
: public LDAPInterface
{
const std::string provider;
const std::string uuid;
const std::string base;
const LDAPAttribute what;
public:
AdminBindInterface(Module* c, const std::string& p, const std::string& u, const std::string& b, const LDAPAttribute& w)
: LDAPInterface(c)
, provider(p)
, uuid(u)
, base(b)
, what(w)
{
}
void OnResult(const LDAPResult& r) override
{
dynamic_reference<LDAPProvider> LDAP(me, provider);
if (LDAP)
{
try
{
const auto sf = INSP_FORMAT("{}={}", what.first, LDAP->EscapeSF(what.second));
LDAP->Search(new SearchInterface(this->creator, provider, uuid), base, sf);
}
catch (const LDAPException& ex)
{
ServerInstance->SNO.WriteToSnoMask('a', "Error searching LDAP server: " + ex.GetReason());
}
}
delete this;
}
void OnError(const LDAPResult& err) override
{
ServerInstance->SNO.WriteToSnoMask('a', "Error binding as manager to LDAP server: " + err.getError());
delete this;
}
};
enum class AuthField
: uint8_t
{
NICKNAME,
USERNAME,
PASSWORD,
};
class ModuleLDAPAuth final
: public Module
{
dynamic_reference<LDAPProvider> LDAP;
BoolExtItem ldapAuthed;
StringExtItem ldapVhost;
std::string base;
std::string attribute;
std::vector<std::string> exemptions;
AuthField field;
public:
ModuleLDAPAuth()
: Module(VF_VENDOR, "Allows connecting users to be authenticated against an LDAP database.")
, LDAP(this, "LDAP")
, ldapAuthed(this, "ldapauth", ExtensionType::USER)
, ldapVhost(this, "ldapauth-vhost", ExtensionType::USER)
{
me = this;
authed = &ldapAuthed;
vhosts = &ldapVhost;
}
void ReadConfig(ConfigStatus& status) override
{
const auto& tag = ServerInstance->Config->ConfValue("ldapauth");
base = tag->getString("baserdn");
attribute = tag->getString("attribute");
killreason = tag->getString("killreason");
vhost = tag->getString("host");
// Set to true if failed connects should be reported to operators
verbose = tag->getBool("verbose");
field = tag->getEnum("field", tag->getBool("useusername") ? AuthField::USERNAME : AuthField::NICKNAME, {
{ "nickname", AuthField::USERNAME },
{ "password", AuthField::PASSWORD },
{ "username", AuthField::NICKNAME },
});
LDAP.SetProvider("LDAP/" + tag->getString("dbid"));
requiredattributes.clear();
for (const auto& [_, rtag] : ServerInstance->Config->ConfTags("ldaprequire"))
{
const std::string attr = rtag->getString("attribute");
const std::string val = rtag->getString("value");
if (!attr.empty() && !val.empty())
requiredattributes.emplace_back(attr, val);
}
exemptions.clear();
for (const auto& [_, etag] : ServerInstance->Config->ConfTags("ldapexemption"))
{
const std::string mask = etag->getString("mask");
if (!mask.empty())
exemptions.push_back(mask);
}
// Begin v3 config compatibility.
for (const auto& [_, wtag] : ServerInstance->Config->ConfTags("ldapwhitelist"))
{
const std::string cidr = wtag->getString("cidr");
if (!cidr.empty())
exemptions.push_back("*!*@" + cidr);
}
irc::spacesepstream nickstream(tag->getString("allowpattern"));
for (std::string nick; nickstream.GetToken(nick); )
exemptions.push_back(nick + "!*@*");
// End v3 config compatibility.
}
void OnUserConnect(LocalUser* user) override
{
std::string* cc = ldapVhost.Get(user);
if (cc)
{
user->ChangeDisplayedHost(*cc);
ldapVhost.Unset(user);
}
}
ModResult OnUserRegister(LocalUser* user) override
{
for (const auto& exemption : exemptions)
{
if (InspIRCd::MatchCIDR(user->GetRealMask(), exemption) || InspIRCd::MatchCIDR(user->GetMask(), exemption))
{
ldapAuthed.Set(user, true);
return MOD_RES_PASSTHRU;
}
}
if (user->password.empty())
{
if (verbose)
ServerInstance->SNO.WriteToSnoMask('c', "Forbidden connection from {} (no password provided)", user->GetRealMask());
ServerInstance->Users.QuitUser(user, killreason);
return MOD_RES_DENY;
}
if (!LDAP)
{
if (verbose)
ServerInstance->SNO.WriteToSnoMask('c', "Forbidden connection from {} (unable to find LDAP provider)", user->GetRealMask());
ServerInstance->Users.QuitUser(user, killreason);
return MOD_RES_DENY;
}
LDAPAttribute what;
what.first = attribute;
switch (field)
{
case AuthField::NICKNAME:
what.second = user->nick;
break;
case AuthField::USERNAME:
what.second = user->GetRealUser();
break;
case AuthField::PASSWORD:
{
auto pos = user->password.find(':');
if (pos == std::string::npos)
{
if (verbose)
ServerInstance->SNO.WriteToSnoMask('c', "Forbidden connection from {} (no username provided)", user->GetRealMask());
ServerInstance->Users.QuitUser(user, killreason);
return MOD_RES_DENY;
}
what.second = user->password.substr(0, pos);
user->password.erase(0, pos + 1);
user->password.shrink_to_fit();
break;
}
}
try
{
LDAP->BindAsManager(new AdminBindInterface(this, LDAP.GetProvider(), user->uuid, base, what));
}
catch (const LDAPException& ex)
{
ServerInstance->SNO.WriteToSnoMask('a', "LDAP exception: " + ex.GetReason());
ServerInstance->Users.QuitUser(user, killreason);
}
return MOD_RES_DENY;
}
ModResult OnCheckReady(LocalUser* user) override
{
return ldapAuthed.Get(user) ? MOD_RES_PASSTHRU : MOD_RES_DENY;
}
};
MODULE_INIT(ModuleLDAPAuth)