View source
<?php
declare (strict_types=1);
namespace Drupal\ldap_authentication\Controller;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\externalauth\Authmap;
use Drupal\ldap_authentication\AuthenticationServers;
use Drupal\ldap_servers\Helper\CredentialsStorage;
use Drupal\ldap_servers\LdapBridgeInterface;
use Drupal\ldap_servers\LdapUserManager;
use Drupal\ldap_servers\Logger\LdapDetailLog;
use Drupal\ldap_servers\LdapUserAttributesInterface;
use Drupal\ldap_user\Processor\DrupalUserProcessor;
use Drupal\user\UserInterface;
use Symfony\Component\Ldap\Entry;
abstract class LoginValidatorBase implements LdapUserAttributesInterface, LoginValidatorInterface {
use StringTranslationTrait;
public const AUTHENTICATION_FAILURE_UNKNOWN = 0;
public const AUTHENTICATION_FAILURE_BIND = 2;
public const AUTHENTICATION_FAILURE_FIND = 3;
public const AUTHENTICATION_FAILURE_DISALLOWED = 4;
public const AUTHENTICATION_FAILURE_CREDENTIALS = 5;
public const AUTHENTICATION_SUCCESS = 6;
protected $authName = FALSE;
protected $drupalUserAuthMapped = FALSE;
protected $drupalUserName = FALSE;
protected $serverDrupalUser;
protected $drupalUser;
protected $ldapEntry;
protected $emailTemplateUsed = FALSE;
protected $emailTemplateTokens = [];
protected $formState;
protected $configFactory;
protected $config;
protected $detailLog;
protected $logger;
protected $entityTypeManager;
protected $moduleHandler;
protected $ldapBridge;
protected $externalAuth;
protected $authenticationServers;
protected $ldapUserManager;
protected $messenger;
protected $drupalUserProcessor;
public function __construct(ConfigFactoryInterface $configFactory, LdapDetailLog $detailLog, LoggerChannelInterface $logger, EntityTypeManagerInterface $entity_type_manager, ModuleHandler $module_handler, LdapBridgeInterface $ldap_bridge, Authmap $external_auth, AuthenticationServers $authentication_servers, LdapUserManager $ldap_user_manager, MessengerInterface $messenger, DrupalUserProcessor $drupal_user_processor) {
$this->configFactory = $configFactory;
$this->config = $configFactory
->get('ldap_authentication.settings');
$this->detailLog = $detailLog;
$this->logger = $logger;
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
$this->ldapBridge = $ldap_bridge;
$this->externalAuth = $external_auth;
$this->authenticationServers = $authentication_servers;
$this->ldapUserManager = $ldap_user_manager;
$this->messenger = $messenger;
$this->drupalUserProcessor = $drupal_user_processor;
}
protected function initializeDrupalUserFromAuthName() : void {
$load_by_name = $this->entityTypeManager
->getStorage('user')
->loadByProperties([
'name' => $this->authName,
]);
$this->drupalUser = $load_by_name ? reset($load_by_name) : NULL;
$authmap_uid = $this->externalAuth
->getUid($this->authName, 'ldap_user');
if (!$this->drupalUser && $authmap_uid) {
$this->drupalUser = $this->entityTypeManager
->getStorage('user')
->load($authmap_uid);
}
if ($this->drupalUser && $authmap_uid) {
$this->drupalUserAuthMapped = TRUE;
}
}
protected function verifyUserAllowed() : bool {
if ($this->config
->get('skipAdministrators')) {
$admin_roles = $this->entityTypeManager
->getStorage('user_role')
->getQuery()
->condition('is_admin', TRUE)
->execute();
if (!empty(array_intersect($this->drupalUser
->getRoles(), $admin_roles))) {
$this->detailLog
->log('%username: Drupal user name maps to an administrative user and this group is excluded from LDAP authentication.', [
'%username' => $this->authName,
], 'ldap_authentication');
return FALSE;
}
}
if ($this->drupalUser
->get('ldap_user_ldap_exclude')
->getString() === '1') {
$this->detailLog
->log('%username: User flagged as excluded.', [
'%username' => $this->authName,
], 'ldap_authentication');
return FALSE;
}
$this->detailLog
->log('%username: Drupal user account found. Continuing on to attempt LDAP authentication.', [
'%username' => $this->authName,
], 'ldap_authentication');
return TRUE;
}
protected function verifyAccountCreation() : bool {
if ($this->configFactory
->get('ldap_user.settings')
->get('acctCreation') === self::ACCOUNT_CREATION_LDAP_BEHAVIOUR || $this->configFactory
->get('user.settings')
->get('register') === UserInterface::REGISTER_VISITORS) {
$this->detailLog
->log('%username: Existing Drupal user account not found. Continuing on to attempt LDAP authentication', [
'%username' => $this->authName,
], 'ldap_authentication');
return TRUE;
}
$this->detailLog
->log('%username: Drupal user account not found and configuration is set to not create new accounts.', [
'%username' => $this->authName,
], 'ldap_authentication');
return FALSE;
}
protected function testUserPassword() : bool {
$loginValid = FALSE;
if ($this->serverDrupalUser
->get('bind_method') === 'user') {
$loginValid = TRUE;
}
else {
$this->ldapBridge
->setServer($this->serverDrupalUser);
CredentialsStorage::storeUserDn($this->ldapEntry
->getDn());
CredentialsStorage::testCredentials(TRUE);
$bindResult = $this->ldapBridge
->bind();
CredentialsStorage::testCredentials(FALSE);
if ($bindResult) {
$loginValid = TRUE;
}
else {
$this->detailLog
->log('%username: Error testing user credentials on server %id with %bind_method.', [
'%username' => $this->authName,
'%bind_method' => $this->serverDrupalUser
->getFormattedBind(),
'%id' => $this->serverDrupalUser
->id(),
], 'ldap_authentication');
}
}
return $loginValid;
}
protected function additionalDebuggingResponse(int $authenticationResult) : TranslatableMarkup {
switch ($authenticationResult) {
case self::AUTHENTICATION_FAILURE_FIND:
$information = $this
->t('(not found)');
break;
case self::AUTHENTICATION_FAILURE_CREDENTIALS:
$information = $this
->t('(wrong credentials)');
break;
case self::AUTHENTICATION_SUCCESS:
$information = $this
->t('(no issue)');
break;
default:
$information = $this
->t('(unknown issue)');
}
return $information;
}
protected function failureResponse(int $authenticationResult) : void {
if ($this->config
->get('authenticationMode') === 'exclusive') {
$this->detailLog
->log('%username: Error raised because failure at LDAP and exclusive authentication is set to true.', [
'%username' => $this->authName,
], 'ldap_authentication');
$this->messenger
->addError($this
->t('Error: %err_text', [
'%err_text' => $this
->authenticationHelpText($authenticationResult),
]));
}
else {
$this->detailLog
->log('%username: Failed LDAP authentication. User may have authenticated successfully by other means in a mixed authentication site.', [
'%username' => $this->authName,
], 'ldap_authentication');
}
}
protected function authenticationHelpText(int $error) : TranslatableMarkup {
switch ($error) {
case self::AUTHENTICATION_FAILURE_BIND:
$msg = $this
->t('Failed to bind to LDAP server');
break;
case self::AUTHENTICATION_FAILURE_DISALLOWED:
$msg = $this
->t('User disallowed');
break;
case self::AUTHENTICATION_FAILURE_FIND:
case self::AUTHENTICATION_FAILURE_CREDENTIALS:
$msg = $this
->t('Sorry, unrecognized username or password.');
break;
case self::AUTHENTICATION_SUCCESS:
$msg = $this
->t('Authentication successful');
break;
default:
$msg = $this
->t('unknown error: @error', [
'@error' => $error,
]);
break;
}
return $msg;
}
public function checkAllowedExcluded(string $authName, Entry $ldap_user) : bool {
foreach ($this->config
->get('excludeIfTextInDn') as $test) {
if (stripos($ldap_user
->getDn(), $test) !== FALSE) {
return FALSE;
}
}
if (count($this->config
->get('allowOnlyIfTextInDn'))) {
$fail = TRUE;
foreach ($this->config
->get('allowOnlyIfTextInDn') as $test) {
if (stripos($ldap_user
->getDn(), $test) !== FALSE) {
$fail = FALSE;
}
}
if ($fail) {
return FALSE;
}
}
if ($this->moduleHandler
->moduleExists('ldap_authorization') && $this->config
->get('excludeIfNoAuthorizations')) {
$user = FALSE;
$id = $this->externalAuth
->getUid($authName, 'ldap_user');
if ($id) {
$user = $this->entityTypeManager
->getStorage('user')
->load($id);
}
if (!$user) {
$user = $this->entityTypeManager
->getStorage('user')
->create([
'name' => $authName,
]);
}
$controller = \Drupal::service('authorization.manager');
$controller
->setUser($user);
$profiles = $this->entityTypeManager
->getStorage('authorization_profile')
->getQuery()
->condition('provider', 'ldap_provider')
->execute();
foreach ($profiles as $profile) {
$controller
->queryIndividualProfile($profile);
}
$authorizations = $controller
->getProcessedAuthorizations();
$controller
->clearAuthorizations();
$valid_profile = FALSE;
foreach ($authorizations as $authorization) {
if (!empty($authorization
->getAuthorizationsApplied())) {
$valid_profile = TRUE;
}
}
if (!$valid_profile) {
$this->messenger
->addWarning($this
->t('The site logon is currently not working due to a configuration error. Please see logs for additional details.'));
$this->logger
->notice('LDAP Authentication is configured to deny users without LDAP Authorization mappings, but 0 LDAP Authorization consumers are configured.');
return FALSE;
}
}
$hook_result = TRUE;
$this->moduleHandler
->alter('ldap_authentication_allowuser_results', $ldap_user, $authName, $hook_result);
if (!$hook_result) {
$this->logger
->notice('Authentication Allow User Result=refused for %name', [
'%name' => $authName,
]);
return FALSE;
}
return TRUE;
}
protected function fixOutdatedEmailAddress() : void {
if ($this->config
->get('emailTemplateUsageNeverUpdate') && $this->emailTemplateUsed) {
return;
}
if (!$this->drupalUser) {
return;
}
if ($this->drupalUser
->get('mail')->value === $this->serverDrupalUser
->deriveEmailFromLdapResponse($this->ldapEntry)) {
return;
}
$update_type = $this->config
->get('emailUpdate');
if (in_array($update_type, [
'update_notify',
'update',
], TRUE)) {
$this->drupalUser
->set('mail', $this->serverDrupalUser
->deriveEmailFromLdapResponse($this->ldapEntry));
if (!$this->drupalUser
->save()) {
$this->logger
->error('Failed to make changes to user %username updated %changed.', [
'%username' => $this->drupalUser
->getAccountName(),
'%changed' => $this->serverDrupalUser
->deriveEmailFromLdapResponse($this->ldapEntry),
]);
}
elseif ($update_type === 'update_notify') {
$this->messenger
->addStatus($this
->t('Your e-mail has been updated to match your current account (%mail).', [
'%mail' => $this->serverDrupalUser
->deriveEmailFromLdapResponse($this->ldapEntry),
]));
}
}
}
protected function updateAuthNameFromPuid() : void {
$puid = $this->serverDrupalUser
->derivePuidFromLdapResponse($this->ldapEntry);
if (!empty($puid)) {
$this->drupalUser = $this->ldapUserManager
->getUserAccountFromPuid($puid);
if ($this->drupalUser) {
$oldName = $this->drupalUser
->getAccountName();
$this->drupalUser
->setUsername($this->drupalUserName);
$this->drupalUser
->save();
$this->externalAuth
->save($this->drupalUser, 'ldap_user', $this->authName);
$this->drupalUserAuthMapped = TRUE;
$this->messenger
->addStatus($this
->t('Your existing account %username has been updated to %new_username.', [
'%username' => $oldName,
'%new_username' => $this->drupalUserName,
]));
}
}
}
protected function validateCommonLoginConstraints() : bool {
if (!$this->authenticationServers
->authenticationServersAvailable()) {
$this->logger
->error('No LDAP servers configured for authentication.');
if ($this->formState) {
$this->formState
->setErrorByName('name', 'Server Error: No LDAP servers configured.');
}
return FALSE;
}
$this
->initializeDrupalUserFromAuthName();
if ($this->drupalUser) {
$result = $this
->verifyUserAllowed();
}
else {
$result = $this
->verifyAccountCreation();
}
return $result;
}
protected function deriveDrupalUserName() : bool {
if ($this->serverDrupalUser
->hasAccountNameAttribute()) {
$user_name_from_attribute = $this->ldapEntry
->getAttribute($this->serverDrupalUser
->getAccountNameAttribute(), FALSE)[0];
if (!$user_name_from_attribute) {
$this->logger
->error('Derived Drupal username from attribute %account_name_attr returned no username for authname %authname.', [
'%authname' => $this->authName,
'%account_name_attr' => $this->serverDrupalUser
->getAccountNameAttribute(),
]);
return FALSE;
}
$this->drupalUserName = $user_name_from_attribute;
}
else {
$this->drupalUserName = $this->authName;
}
$this
->prepareEmailTemplateToken();
return TRUE;
}
protected function prepareEmailTemplateToken() : void {
$this->emailTemplateTokens = [
'@username' => $this->drupalUserName,
];
if (!empty($this->config
->get('emailTemplate'))) {
$handling = $this->config
->get('emailTemplateHandling');
if ($handling === 'if_empty' && empty($this->serverDrupalUser
->deriveEmailFromLdapResponse($this->ldapEntry)) || $handling === 'always') {
$this
->replaceUserMailWithTemplate();
$this->detailLog
->log('Using template generated email for %username', [
'%username' => $this->drupalUserName,
], 'ldap_authentication');
$this->emailTemplateUsed = TRUE;
}
}
}
protected function matchExistingUserWithLdap() : bool {
if ($this->configFactory
->get('ldap_user.settings')
->get('userConflictResolve') === self::USER_CONFLICT_LOG) {
$users = $this->entityTypeManager
->getStorage('user')
->loadByProperties([
'mail' => $this->serverDrupalUser
->deriveEmailFromLdapResponse($this->ldapEntry),
]);
if (count($users) > 0) {
$account_with_same_email = reset($users);
$this->logger
->error('LDAP user with DN %dn has a naming conflict with a local Drupal user %conflict_name', [
'%dn' => $this->ldapEntry
->getDn(),
'%conflict_name' => $account_with_same_email
->getAccountName(),
]);
}
$this->messenger
->addError($this
->t('Another user already exists in the system with the same login name. You should contact the system administrator in order to solve this conflict.'));
return FALSE;
}
$this->externalAuth
->save($this->drupalUser, 'ldap_user', $this->authName);
$this->drupalUserAuthMapped = TRUE;
$this->detailLog
->log('Set authmap for LDAP user %username', [
'%username' => $this->authName,
], 'ldap_authentication');
return TRUE;
}
protected function replaceUserMailWithTemplate() : void {
$template = '@username@localhost';
if (!empty($this->config
->get('emailTemplate'))) {
$template = $this->config
->get('emailTemplate');
}
$this->ldapEntry
->setAttribute($this->serverDrupalUser
->get('mail_attr'), [
(string) new FormattableMarkup($template, $this->emailTemplateTokens),
]);
}
protected function provisionDrupalUser() : bool {
$users = $this->entityTypeManager
->getStorage('user')
->loadByProperties([
'mail' => $this->serverDrupalUser
->deriveEmailFromLdapResponse($this->ldapEntry),
]);
$accountDuplicateMail = $users ? reset($users) : FALSE;
if ($accountDuplicateMail) {
$emailAvailable = FALSE;
if (!$this->emailTemplateUsed && $this->config
->get('emailTemplateUsageResolveConflict')) {
$this->detailLog
->log('Conflict detected, using template generated email for %username', [
'%duplicate_name' => $accountDuplicateMail
->getAccountName(),
], 'ldap_authentication');
$this
->replaceUserMailWithTemplate();
$this->emailTemplateUsed = TRUE;
$users = $this->entityTypeManager
->getStorage('user')
->loadByProperties([
'mail' => $this->serverDrupalUser
->deriveEmailFromLdapResponse($this->ldapEntry),
]);
$accountDuplicateMail = $users ? reset($users) : FALSE;
if ($accountDuplicateMail) {
$emailAvailable = FALSE;
}
else {
$emailAvailable = TRUE;
}
}
if (!$emailAvailable) {
$this->logger
->error('LDAP user with DN %dn has email address (%mail) conflict with a Drupal user %duplicate_name', [
'%dn' => $this->ldapEntry
->getDn(),
'%duplicate_name' => $accountDuplicateMail
->getAccountName(),
]);
$this->messenger
->addError($this
->t('Another user already exists in the system with the same email address. You should contact the system administrator in order to solve this conflict.'));
return FALSE;
}
}
$triggers = $this->configFactory
->get('ldap_user.settings')
->get('drupalAcctProvisionTriggers');
if (!in_array(self::PROVISION_DRUPAL_USER_ON_USER_AUTHENTICATION, $triggers, TRUE)) {
$this->logger
->error('Drupal account for authname=%authname does not exist and provisioning of Drupal accounts on authentication is not enabled', [
'%authname' => $this->authName,
]);
return FALSE;
}
if ($this->configFactory
->get('ldap_user.settings')
->get('acctCreation') === self::ACCOUNT_CREATION_USER_SETTINGS_FOR_LDAP && $this->configFactory
->get('user.settings')
->get('register') === UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) {
$user_values = [
'name' => $this->drupalUserName,
'status' => 0,
];
}
else {
$user_values = [
'name' => $this->drupalUserName,
'status' => 1,
];
}
if ($this->emailTemplateUsed) {
$user_values['mail'] = $this->serverDrupalUser
->deriveEmailFromLdapResponse($this->ldapEntry);
}
$result = $this->drupalUserProcessor
->createDrupalUserFromLdapEntry($user_values);
if (!$result) {
$this->logger
->error('Failed to find or create %drupal_accountname on logon.', [
'%drupal_accountname' => $this->drupalUserName,
]);
if ($this->formState) {
$this->formState
->setErrorByName('name', $this
->t('Server Error: Failed to create Drupal user account for %drupal_accountname', [
'%drupal_accountname' => $this->drupalUserName,
]));
}
return FALSE;
}
$this->drupalUser = $this->drupalUserProcessor
->getUserAccount();
return TRUE;
}
protected function bindToServer() : int {
if ($this->serverDrupalUser
->get('bind_method') === 'user') {
return $this
->bindToServerAsUser();
}
$bindResult = $this->ldapBridge
->bind();
if (!$bindResult) {
$this->detailLog
->log('%username: Unsuccessful with server %id (bind method: %bind_method)', [
'%username' => $this->authName,
'%id' => $this->serverDrupalUser
->id(),
'%bind_method' => $this->serverDrupalUser
->get('bind_method'),
], 'ldap_authentication');
return self::AUTHENTICATION_FAILURE_BIND;
}
return self::AUTHENTICATION_SUCCESS;
}
protected function bindToServerAsUser() : int {
$bindResult = FALSE;
foreach ($this->serverDrupalUser
->getBaseDn() as $base_dn) {
$search = [
'%basedn',
'%username',
];
$replace = [
$base_dn,
$this->authName,
];
CredentialsStorage::storeUserDn(str_replace($search, $replace, $this->serverDrupalUser
->getUserDnExpression()));
CredentialsStorage::testCredentials(TRUE);
$bindResult = $this->ldapBridge
->bind();
if ($bindResult) {
break;
}
}
if (!$bindResult) {
$this->detailLog
->log('%username: Unsuccessful with server %id (bind method: %bind_method)', [
'%username' => $this->authName,
'%id' => $this->serverDrupalUser
->id(),
'%bind_method' => $this->serverDrupalUser
->get('bind_method'),
], 'ldap_authentication');
return self::AUTHENTICATION_FAILURE_CREDENTIALS;
}
return self::AUTHENTICATION_SUCCESS;
}
public function getDrupalUser() : ?UserInterface {
return $this->drupalUser;
}
}