2017-11-18 10:33:03 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
// easier to rewrite for Active Directory than to bash it into existing LDAP implementation
|
|
|
|
|
|
|
|
// disable certificate checking before connect if required
|
2020-09-21 12:54:51 +00:00
|
|
|
|
2017-11-18 10:33:03 +00:00
|
|
|
namespace LibreNMS\Authentication;
|
|
|
|
|
|
|
|
use LibreNMS\Config;
|
|
|
|
use LibreNMS\Exceptions\AuthenticationException;
|
2019-02-21 18:08:35 +00:00
|
|
|
use LibreNMS\Exceptions\LdapMissingException;
|
2017-11-18 10:33:03 +00:00
|
|
|
|
|
|
|
class ActiveDirectoryAuthorizer extends AuthorizerBase
|
|
|
|
{
|
2018-09-18 12:57:23 +00:00
|
|
|
use ActiveDirectoryCommon;
|
|
|
|
|
2021-04-01 15:35:18 +00:00
|
|
|
protected static $CAN_UPDATE_PASSWORDS = false;
|
2018-02-06 21:20:34 +00:00
|
|
|
|
2017-11-18 10:33:03 +00:00
|
|
|
protected $ldap_connection;
|
2017-11-28 15:19:34 +00:00
|
|
|
protected $is_bound = false; // this variable tracks if bind has been called so we don't call it multiple times
|
2017-11-18 10:33:03 +00:00
|
|
|
|
2019-03-05 06:24:14 +00:00
|
|
|
public function authenticate($credentials)
|
2017-11-18 10:33:03 +00:00
|
|
|
{
|
2017-11-28 15:19:34 +00:00
|
|
|
$this->connect();
|
|
|
|
|
2017-11-18 10:33:03 +00:00
|
|
|
if ($this->ldap_connection) {
|
|
|
|
// bind with sAMAccountName instead of full LDAP DN
|
2019-03-05 06:24:14 +00:00
|
|
|
if (! empty($credentials['username']) && ! empty($credentials['password']) && ldap_bind($this->ldap_connection, $credentials['username'] . '@' . Config::get('auth_ad_domain'), $credentials['password'])) {
|
2017-11-28 15:19:34 +00:00
|
|
|
$this->is_bound = true;
|
2017-11-18 10:33:03 +00:00
|
|
|
// group membership in one of the configured groups is required
|
|
|
|
if (Config::get('auth_ad_require_groupmembership', true)) {
|
|
|
|
// cycle through defined groups, test for memberOf-ship
|
|
|
|
foreach (Config::get('auth_ad_groups', []) as $group => $level) {
|
2019-03-05 06:24:14 +00:00
|
|
|
if ($this->userInGroup($credentials['username'], $group)) {
|
2017-11-18 10:33:03 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// failed to find user
|
|
|
|
if (Config::get('auth_ad_debug', false)) {
|
|
|
|
throw new AuthenticationException('User is not in one of the required groups or user/group is outside the base dn');
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new AuthenticationException();
|
|
|
|
} else {
|
|
|
|
// group membership is not required and user is valid
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-05 06:24:14 +00:00
|
|
|
if (empty($credentials['password'])) {
|
2017-11-18 10:33:03 +00:00
|
|
|
throw new AuthenticationException('A password is required');
|
|
|
|
} elseif (Config::get('auth_ad_debug', false)) {
|
|
|
|
ldap_get_option($this->ldap_connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extended_error);
|
|
|
|
throw new AuthenticationException(ldap_error($this->ldap_connection) . '<br />' . $extended_error);
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new AuthenticationException(ldap_error($this->ldap_connection));
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function userInGroup($username, $groupname)
|
|
|
|
{
|
2018-10-11 19:29:57 +00:00
|
|
|
$connection = $this->getConnection();
|
|
|
|
|
2017-11-18 10:33:03 +00:00
|
|
|
// check if user is member of the given group or nested groups
|
|
|
|
$search_filter = "(&(objectClass=group)(cn=$groupname))";
|
|
|
|
|
|
|
|
// get DN for auth_ad_group
|
|
|
|
$search = ldap_search(
|
2018-10-11 19:29:57 +00:00
|
|
|
$connection,
|
2017-11-18 10:33:03 +00:00
|
|
|
Config::get('auth_ad_base_dn'),
|
|
|
|
$search_filter,
|
|
|
|
['cn']
|
|
|
|
);
|
2018-10-11 19:29:57 +00:00
|
|
|
$result = ldap_get_entries($connection, $search);
|
2017-11-18 10:33:03 +00:00
|
|
|
|
|
|
|
if ($result == false || $result['count'] !== 1) {
|
|
|
|
if (Config::get('auth_ad_debug', false)) {
|
|
|
|
if ($result == false) {
|
2022-09-06 21:43:51 +00:00
|
|
|
throw new AuthenticationException("LDAP query failed for group '$groupname' using filter '$search_filter', last LDAP error: " . ldap_error($connection));
|
|
|
|
} elseif ((int) $result['count'] == 0) {
|
2017-11-18 10:33:03 +00:00
|
|
|
throw new AuthenticationException("Failed to find group matching '$groupname' using filter '$search_filter'");
|
2022-09-06 21:43:51 +00:00
|
|
|
} elseif ((int) $result['count'] > 1) {
|
2017-11-18 10:33:03 +00:00
|
|
|
throw new AuthenticationException("Multiple groups returned for '$groupname' using filter '$search_filter'");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new AuthenticationException();
|
|
|
|
}
|
|
|
|
|
2021-02-02 06:13:48 +00:00
|
|
|
// special character handling
|
2021-06-11 12:58:34 +00:00
|
|
|
$group_dn = addcslashes($result[0]['dn'], '()#');
|
2017-11-18 10:33:03 +00:00
|
|
|
|
|
|
|
$search = ldap_search(
|
2018-10-11 19:29:57 +00:00
|
|
|
$connection,
|
2017-11-18 10:33:03 +00:00
|
|
|
Config::get('auth_ad_base_dn'),
|
|
|
|
// add 'LDAP_MATCHING_RULE_IN_CHAIN to the user filter to search for $username in nested $group_dn
|
|
|
|
// limiting to "DN" for shorter array
|
2018-09-18 12:57:23 +00:00
|
|
|
'(&' . $this->userFilter($username) . "(memberOf:1.2.840.113556.1.4.1941:=$group_dn))",
|
2017-11-18 10:33:03 +00:00
|
|
|
['DN']
|
|
|
|
);
|
2018-10-11 19:29:57 +00:00
|
|
|
$entries = ldap_get_entries($connection, $search);
|
2017-11-18 10:33:03 +00:00
|
|
|
|
2022-09-06 21:43:51 +00:00
|
|
|
return (int) $entries['count'] > 0;
|
2017-11-18 10:33:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function userExists($username, $throw_exception = false)
|
|
|
|
{
|
2018-10-11 19:29:57 +00:00
|
|
|
$connection = $this->getConnection();
|
2017-11-18 10:33:03 +00:00
|
|
|
|
|
|
|
$search = ldap_search(
|
2018-10-11 19:29:57 +00:00
|
|
|
$connection,
|
2017-11-18 10:33:03 +00:00
|
|
|
Config::get('auth_ad_base_dn'),
|
2018-09-18 12:57:23 +00:00
|
|
|
$this->userFilter($username),
|
2017-11-18 10:33:03 +00:00
|
|
|
['samaccountname']
|
|
|
|
);
|
2018-10-11 19:29:57 +00:00
|
|
|
$entries = ldap_get_entries($connection, $search);
|
2017-11-18 10:33:03 +00:00
|
|
|
|
|
|
|
if ($entries['count']) {
|
2021-02-02 06:13:48 +00:00
|
|
|
return true;
|
2017-11-18 10:33:03 +00:00
|
|
|
}
|
|
|
|
|
2021-02-02 06:13:48 +00:00
|
|
|
return false;
|
2017-11-18 10:33:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function getUserlevel($username)
|
|
|
|
{
|
|
|
|
$userlevel = 0;
|
|
|
|
if (! Config::get('auth_ad_require_groupmembership', true)) {
|
|
|
|
if (Config::get('auth_ad_global_read', false)) {
|
|
|
|
$userlevel = 5;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// cycle through defined groups, test for memberOf-ship
|
|
|
|
foreach (Config::get('auth_ad_groups', []) as $group => $level) {
|
|
|
|
try {
|
|
|
|
if ($this->userInGroup($username, $group)) {
|
|
|
|
$userlevel = max($userlevel, $level['level']);
|
|
|
|
}
|
|
|
|
} catch (AuthenticationException $e) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $userlevel;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getUserid($username)
|
|
|
|
{
|
2018-10-11 19:29:57 +00:00
|
|
|
$connection = $this->getConnection();
|
2017-11-18 10:33:03 +00:00
|
|
|
|
|
|
|
$attributes = ['objectsid'];
|
|
|
|
$search = ldap_search(
|
2018-10-11 19:29:57 +00:00
|
|
|
$connection,
|
2017-11-18 10:33:03 +00:00
|
|
|
Config::get('auth_ad_base_dn'),
|
2018-09-18 12:57:23 +00:00
|
|
|
$this->userFilter($username),
|
2017-11-18 10:33:03 +00:00
|
|
|
$attributes
|
|
|
|
);
|
|
|
|
|
2022-08-14 14:14:12 +00:00
|
|
|
if ($search !== false) {
|
|
|
|
$entries = ldap_get_entries($connection, $search);
|
|
|
|
|
|
|
|
if ($entries !== false && $entries['count']) {
|
|
|
|
return $this->getUseridFromSid($this->sidFromLdap($entries[0]['objectsid'][0]));
|
|
|
|
}
|
2017-11-18 10:33:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Bind to AD with the bind user if available, otherwise anonymous bind
|
|
|
|
*/
|
2018-10-11 19:29:57 +00:00
|
|
|
protected function init()
|
2017-11-18 10:33:03 +00:00
|
|
|
{
|
2018-10-11 19:29:57 +00:00
|
|
|
if ($this->ldap_connection) {
|
|
|
|
return;
|
2017-11-18 10:33:03 +00:00
|
|
|
}
|
|
|
|
|
2018-10-11 19:29:57 +00:00
|
|
|
$this->connect();
|
|
|
|
$this->bind();
|
2017-11-18 10:33:03 +00:00
|
|
|
}
|
2017-11-28 15:19:34 +00:00
|
|
|
|
|
|
|
protected function connect()
|
|
|
|
{
|
|
|
|
if ($this->ldap_connection) {
|
|
|
|
// no need to re-connect
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (! function_exists('ldap_connect')) {
|
2019-02-21 18:08:35 +00:00
|
|
|
throw new LdapMissingException();
|
2017-11-28 15:19:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (Config::has('auth_ad_check_certificates') &&
|
|
|
|
! Config::get('auth_ad_check_certificates')) {
|
|
|
|
putenv('LDAPTLS_REQCERT=never');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Config::has('auth_ad_check_certificates') && Config::get('auth_ad_debug')) {
|
|
|
|
ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->ldap_connection = @ldap_connect(Config::get('auth_ad_url'));
|
|
|
|
|
|
|
|
// disable referrals and force ldap version to 3
|
|
|
|
ldap_set_option($this->ldap_connection, LDAP_OPT_REFERRALS, 0);
|
|
|
|
ldap_set_option($this->ldap_connection, LDAP_OPT_PROTOCOL_VERSION, 3);
|
2022-07-05 19:53:29 +00:00
|
|
|
|
|
|
|
$starttls = Config::get('auth_ad_starttls');
|
|
|
|
if ($starttls == 'optional' || $starttls == 'required') {
|
|
|
|
$tls = ldap_start_tls($this->ldap_connection);
|
|
|
|
if ($starttls == 'required' && $tls === false) {
|
|
|
|
throw new AuthenticationException('Fatal error: LDAP TLS required but not successfully negotiated:' . ldap_error($this->ldap_connection));
|
|
|
|
}
|
|
|
|
}
|
2017-11-28 15:19:34 +00:00
|
|
|
}
|
2018-04-11 09:06:46 +00:00
|
|
|
|
2019-03-05 18:14:21 +00:00
|
|
|
public function bind($credentials = [])
|
2018-10-11 19:29:57 +00:00
|
|
|
{
|
|
|
|
if (! $this->ldap_connection) {
|
|
|
|
$this->connect();
|
|
|
|
}
|
|
|
|
|
2019-03-05 06:24:14 +00:00
|
|
|
$username = $credentials['username'] ?? null;
|
|
|
|
$password = $credentials['password'] ?? null;
|
|
|
|
|
2018-10-11 19:29:57 +00:00
|
|
|
if (Config::has('auth_ad_binduser') && Config::has('auth_ad_bindpassword')) {
|
|
|
|
$username = Config::get('auth_ad_binduser');
|
|
|
|
$password = Config::get('auth_ad_bindpassword');
|
|
|
|
}
|
|
|
|
$username .= '@' . Config::get('auth_ad_domain');
|
|
|
|
|
|
|
|
ldap_set_option($this->ldap_connection, LDAP_OPT_NETWORK_TIMEOUT, Config::get('auth_ad_timeout', 5));
|
|
|
|
$bind_result = ldap_bind($this->ldap_connection, $username, $password);
|
|
|
|
ldap_set_option($this->ldap_connection, LDAP_OPT_NETWORK_TIMEOUT, -1); // restore timeout
|
|
|
|
|
|
|
|
if ($bind_result) {
|
2021-02-02 06:13:48 +00:00
|
|
|
return $bind_result;
|
2018-10-11 19:29:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ldap_set_option($this->ldap_connection, LDAP_OPT_NETWORK_TIMEOUT, Config::get('auth_ad_timeout', 5));
|
|
|
|
ldap_bind($this->ldap_connection);
|
|
|
|
ldap_set_option($this->ldap_connection, LDAP_OPT_NETWORK_TIMEOUT, -1); // restore timeout
|
|
|
|
}
|
|
|
|
|
2018-09-18 12:57:23 +00:00
|
|
|
protected function getConnection()
|
2018-04-11 09:06:46 +00:00
|
|
|
{
|
2018-10-11 19:29:57 +00:00
|
|
|
$this->init(); // make sure connected and bound
|
2020-09-21 12:54:51 +00:00
|
|
|
|
2018-09-18 12:57:23 +00:00
|
|
|
return $this->ldap_connection;
|
2018-04-11 09:06:46 +00:00
|
|
|
}
|
2017-11-18 10:33:03 +00:00
|
|
|
}
|