View source
<?php
declare (strict_types=1);
namespace Drupal\ldap_user\Processor;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\externalauth\Authmap;
use Drupal\ldap_servers\LdapUserManager;
use Drupal\ldap_servers\Logger\LdapDetailLog;
use Drupal\ldap_servers\Processor\TokenProcessor;
use Drupal\ldap_servers\LdapUserAttributesInterface;
use Drupal\ldap_user\Event\LdapUserLoginEvent;
use Drupal\ldap_user\FieldProvider;
use Drupal\Core\Utility\Token;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use function in_array;
class DrupalUserProcessor implements LdapUserAttributesInterface {
use StringTranslationTrait;
protected $logger;
protected $config;
protected $configAuthentication;
protected $detailLog;
protected $tokenProcessor;
protected $externalAuth;
protected $entityTypeManager;
protected $fileSystem;
protected $token;
protected $moduleHandler;
protected $currentUser;
protected $fieldProvider;
private $account;
private $ldapEntry;
private $server;
protected $ldapUserManager;
protected $eventDispatcher;
protected $messenger;
public function __construct(LoggerInterface $logger, ConfigFactory $config_factory, LdapDetailLog $detail_log, TokenProcessor $token_processor, Authmap $authmap, EntityTypeManagerInterface $entity_type_manager, FileSystemInterface $file_system, Token $token, ModuleHandler $module_handler, AccountInterface $current_user, LdapUserManager $ldap_user_manager, EventDispatcherInterface $event_dispatcher, FieldProvider $field_provider, MessengerInterface $messenger) {
$this->logger = $logger;
$this->config = $config_factory
->get('ldap_user.settings');
$this->configAuthentication = $config_factory
->get('ldap_authentication.settings');
$this->detailLog = $detail_log;
$this->tokenProcessor = $token_processor;
$this->externalAuth = $authmap;
$this->entityTypeManager = $entity_type_manager;
$this->fileSystem = $file_system;
$this->token = $token;
$this->moduleHandler = $module_handler;
$this->currentUser = $current_user;
$this->ldapUserManager = $ldap_user_manager;
$this->eventDispatcher = $event_dispatcher;
$this->fieldProvider = $field_provider;
$this->messenger = $messenger;
}
public function excludeUser(UserInterface $account) : bool {
if ($this->configAuthentication
->get('skipAdministrators')) {
$admin_roles = $this->entityTypeManager
->getStorage('user_role')
->getQuery()
->condition('is_admin', TRUE)
->execute();
if (!empty(array_intersect($account
->getRoles(), $admin_roles))) {
return TRUE;
}
}
return $account
->get('ldap_user_ldap_exclude')
->getString() === '1';
}
public function getUserAccount() : ?User {
return $this->account;
}
public function ldapAssociateDrupalAccount(string $drupal_username) : bool {
if (!$this->config
->get('drupalAcctProvisionServer')) {
return FALSE;
}
$ldap_server = $this->entityTypeManager
->getStorage('ldap_server')
->load($this->config
->get('drupalAcctProvisionServer'));
$load_by_name = $this->entityTypeManager
->getStorage('user')
->loadByProperties([
'name' => $drupal_username,
]);
if (!$load_by_name) {
$this->logger
->error('Failed to LDAP associate Drupal account %drupal_username because account not found', [
'%drupal_username' => $drupal_username,
]);
return FALSE;
}
$this->account = reset($load_by_name);
$this->ldapEntry = $this->ldapUserManager
->matchUsernameToExistingLdapEntry($drupal_username);
if (!$this->ldapEntry) {
$this->logger
->error('Failed to LDAP associate Drupal account %drupal_username because corresponding LDAP entry not found', [
'%drupal_username' => $drupal_username,
]);
return FALSE;
}
$persistent_uid = $ldap_server
->derivePuidFromLdapResponse($this->ldapEntry);
if (!empty($persistent_uid)) {
$this->account
->set('ldap_user_puid', $persistent_uid);
}
$this->account
->set('ldap_user_puid_property', $ldap_server
->getUniquePersistentAttribute());
$this->account
->set('ldap_user_puid_sid', $ldap_server
->id());
$this->account
->set('ldap_user_current_dn', $this->ldapEntry
->getDn());
$this->account
->set('ldap_user_last_checked', time());
$this->account
->set('ldap_user_ldap_exclude', 0);
$this
->saveAccount();
$this->externalAuth
->save($this->account, 'ldap_user', $this->account
->getAccountName());
return TRUE;
}
public function createDrupalUserFromLdapEntry(array $user_data) : bool {
$this->account = $this->entityTypeManager
->getStorage('user')
->create($user_data);
$this->server = $this->entityTypeManager
->getStorage('ldap_server')
->load($this->config
->get('drupalAcctProvisionServer'));
if ($this->config
->get('drupalAcctProvisionServer')) {
$this->ldapUserManager
->setServer($this->server);
$this->ldapEntry = $this->ldapUserManager
->getUserDataByIdentifier($this->account
->getAccountName());
}
if (!$this->ldapEntry) {
$this->detailLog
->log('@username: Failed to find associated LDAP entry for username in provision.', [
'@username' => $this->account
->getAccountName(),
], 'ldap-user');
return FALSE;
}
$params = [
'account' => $this->account,
'prov_event' => self::EVENT_CREATE_DRUPAL_USER,
'module' => 'ldap_user',
'function' => 'createDrupalUserFromLdapEntry',
'direction' => self::PROVISION_TO_DRUPAL,
];
$this->moduleHandler
->alter('ldap_entry', $this->ldapEntry, $params);
$persistentUid = $this->server
->derivePuidFromLdapResponse($this->ldapEntry);
$accountFromPuid = !empty($persistentUid) ? $this->ldapUserManager
->getUserAccountFromPuid($persistentUid) : FALSE;
if ($accountFromPuid) {
$this
->updateExistingAccountByPersistentUid($accountFromPuid);
}
else {
$this
->createDrupalUser();
}
return TRUE;
}
public function ldapExcludeDrupalAccount(string $drupalUsername) : bool {
$accounts = $this->entityTypeManager
->getStorage('user')
->loadByProperties([
'name' => $drupalUsername,
]);
if (!$accounts) {
$this->logger
->error('Failed to exclude user from LDAP association because Drupal account %username was not found', [
'%username' => $drupalUsername,
]);
return FALSE;
}
$account = reset($accounts);
$account
->set('ldap_user_ldap_exclude', 1);
return (bool) $account
->save();
}
public function drupalUserUpdate(UserInterface $account) : void {
$this->account = $account;
if ($this
->excludeUser($this->account)) {
return;
}
$server = $this->config
->get('drupalAcctProvisionServer');
$triggers = $this->config
->get('drupalAcctProvisionTriggers');
if ($server && in_array(self::PROVISION_DRUPAL_USER_ON_USER_UPDATE_CREATE, $triggers, TRUE)) {
$this
->syncToDrupalAccount();
}
}
public function drupalUserLogsIn(UserInterface $account) : void {
$this->account = $account;
if ($this
->excludeUser($this->account)) {
return;
}
$triggers = $this->config
->get('drupalAcctProvisionTriggers');
$server = $this->config
->get('drupalAcctProvisionServer');
if ($server && in_array(self::PROVISION_DRUPAL_USER_ON_USER_AUTHENTICATION, $triggers, TRUE)) {
$this
->syncToDrupalAccount();
}
$event = new LdapUserLoginEvent($account);
if (version_compare(\Drupal::VERSION, '9.1', '>=')) {
$this->eventDispatcher
->dispatch($event, LdapUserLoginEvent::EVENT_NAME);
}
else {
$this->eventDispatcher
->dispatch(LdapUserLoginEvent::EVENT_NAME, $event);
}
}
private function createDrupalUser() : void {
$this->account
->enforceIsNew();
$this
->applyAttributesToAccountOnCreate();
$tokens = [
'%drupal_username' => $this->account
->getAccountName(),
];
if (empty($this->account
->getAccountName())) {
$this->messenger
->addError($this
->t('User account creation failed because of invalid, empty derived Drupal username.'));
$this->logger
->error('Failed to create Drupal account %drupal_username because Drupal username could not be derived.', $tokens);
return;
}
if (!($mail = $this->account
->getEmail())) {
$this->messenger
->addError($this
->t('User account creation failed because of invalid, empty derived email address.'));
$this->logger
->error('Failed to create Drupal account %drupal_username because email address could not be derived by LDAP User module', $tokens);
return;
}
$users = $this->entityTypeManager
->getStorage('user')
->loadByProperties([
'mail' => $mail,
]);
$account_with_same_email = $users ? reset($users) : FALSE;
if ($account_with_same_email) {
$this->logger
->error('LDAP user %drupal_username has email address (%email) conflict with a Drupal user %duplicate_name', [
'%drupal_username' => $this->account
->getAccountName(),
'%email' => $mail,
'%duplicate_name' => $account_with_same_email
->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;
}
$this
->saveAccount();
$this->externalAuth
->save($this->account, 'ldap_user', $this->account
->getAccountName());
}
private function updateExistingAccountByPersistentUid(UserInterface $accountFromPuid) : void {
$this->account = $accountFromPuid;
$this->externalAuth
->save($this->account, 'ldap_user', $this->account
->getAccountName());
$this
->syncToDrupalAccount();
$this
->saveAccount();
}
private function userPictureFromLdapEntry() : ?array {
$picture_attribute = $this->server
->getPictureAttribute();
if (!$this->ldapEntry || !$picture_attribute || !$this->ldapEntry
->hasAttribute($picture_attribute, FALSE)) {
return NULL;
}
$ldapUserPicture = $this->ldapEntry
->getAttribute($picture_attribute, FALSE)[0];
$currentUserPicture = $this->account
->get('user_picture')
->getValue();
if (empty($currentUserPicture)) {
return $this
->saveUserPicture($this->account
->get('user_picture'), $ldapUserPicture);
}
$file = $this->entityTypeManager
->getStorage('file')
->load($currentUserPicture[0]['target_id']);
if ($file && file_exists($file
->getFileUri())) {
$file_data = file_get_contents($file
->getFileUri());
if (md5($file_data) === md5($ldapUserPicture)) {
return NULL;
}
}
return $this
->saveUserPicture($this->account
->get('user_picture'), $ldapUserPicture);
}
private function saveUserPicture(FieldItemListInterface $field, string $ldapUserPicture) : ?array {
$fileName = uniqid('', FALSE);
$unmanagedFile = $this->fileSystem
->getTempDirectory() . '/' . $fileName;
file_put_contents($unmanagedFile, $ldapUserPicture);
$image_type = exif_imagetype($unmanagedFile);
$extension = image_type_to_extension($image_type, FALSE);
unlink($unmanagedFile);
$fieldSettings = $field
->getFieldDefinition()
->getItemDefinition()
->getSettings();
$directory = $this->token
->replace($fieldSettings['file_directory']);
$fullDirectoryPath = $fieldSettings['uri_scheme'] . '://' . $directory;
$realpath = $this->fileSystem
->realpath($fullDirectoryPath);
if ($realpath && !is_dir((string) $realpath)) {
$this->fileSystem
->mkdir($fullDirectoryPath, NULL, TRUE);
}
$managed_file = file_save_data($ldapUserPicture, $fullDirectoryPath . '/' . $fileName . '.' . $extension);
$validators = [
'file_validate_is_image' => [],
'file_validate_image_resolution' => [
$fieldSettings['max_resolution'],
],
'file_validate_size' => [
$fieldSettings['max_filesize'],
],
];
$errors = file_validate($managed_file, $validators);
if ($managed_file && empty(file_validate($managed_file, $validators))) {
return [
'target_id' => $managed_file
->id(),
];
}
foreach ($errors as $error) {
$this->detailLog
->log('File upload error for user image with validation error @error', [
'@error' => $error,
]);
}
return NULL;
}
private function saveAccount() : void {
$this->account
->save();
}
private function applyAttributesToAccount() : void {
$this->fieldProvider
->loadAttributes(self::PROVISION_TO_DRUPAL, $this->server);
$this
->setLdapBaseFields(self::EVENT_SYNC_TO_DRUPAL_USER);
$this
->setUserDefinedMappings(self::EVENT_SYNC_TO_DRUPAL_USER);
$context = [
'ldap_server' => $this->server,
'prov_event' => self::EVENT_SYNC_TO_DRUPAL_USER,
];
$this->moduleHandler
->alter('ldap_user_edit_user', $this->account, $this->ldapEntry, $context);
$this->account
->set('ldap_user_last_checked', time());
}
private function applyAttributesToAccountOnCreate() : void {
$this->fieldProvider
->loadAttributes(self::PROVISION_TO_DRUPAL, $this->server);
$this
->setLdapBaseFields(self::EVENT_CREATE_DRUPAL_USER);
$this
->setFieldsOnDrupalUserCreation();
$this
->setUserDefinedMappings(self::EVENT_CREATE_DRUPAL_USER);
$context = [
'ldap_server' => $this->server,
'prov_event' => self::EVENT_CREATE_DRUPAL_USER,
];
$this->moduleHandler
->alter('ldap_user_edit_user', $this->account, $this->ldapEntry, $context);
$this->account
->set('ldap_user_last_checked', time());
}
private function syncToDrupalAccount() : bool {
if (!$this->account instanceof UserInterface) {
$this->logger
->notice('Invalid selection passed to syncToDrupalAccount.');
return FALSE;
}
if (property_exists($this->account, 'ldap_synced')) {
return FALSE;
}
if (!$this->ldapEntry && $this->config
->get('drupalAcctProvisionServer')) {
$this->ldapUserManager
->setServerById($this->config
->get('drupalAcctProvisionServer'));
$this->ldapEntry = $this->ldapUserManager
->getUserDataByAccount($this->account);
}
if (!$this->ldapEntry) {
return FALSE;
}
if ($this->config
->get('drupalAcctProvisionServer')) {
$this->server = $this->entityTypeManager
->getStorage('ldap_server')
->load($this->config
->get('drupalAcctProvisionServer'));
$this
->applyAttributesToAccount();
$this->account->ldap_synced = TRUE;
}
return TRUE;
}
private function setFieldsOnDrupalUserCreation() : void {
$derived_mail = $this->server
->deriveEmailFromLdapResponse($this->ldapEntry);
if (!$this->account
->getEmail()) {
$this->account
->set('mail', $derived_mail);
}
if (!$this->account
->getPassword()) {
if (version_compare(\Drupal::VERSION, '9.1', '>=')) {
$this->account
->set('pass', \Drupal::service('password_generator')
->generate(20));
}
else {
$this->account
->set('pass', user_password(20));
}
}
if (!$this->account
->getInitialEmail()) {
$this->account
->set('init', $derived_mail);
}
if (!$this->account
->isBlocked()) {
$this->account
->set('status', 1);
}
}
private function setLdapBaseFields(string $event) : void {
if ($this->fieldProvider
->attributeIsSyncedOnEvent('[property.name]', $event)) {
$this->account
->set('name', $this->server
->deriveUsernameFromLdapResponse($this->ldapEntry));
}
if ($this->fieldProvider
->attributeIsSyncedOnEvent('[property.mail]', $event)) {
$derived_mail = $this->server
->deriveEmailFromLdapResponse($this->ldapEntry);
if (!empty($derived_mail)) {
$this->account
->set('mail', $derived_mail);
}
}
if ($this->fieldProvider
->attributeIsSyncedOnEvent('[property.picture]', $event)) {
$picture = $this
->userPictureFromLdapEntry();
if ($picture) {
$this->account
->set('user_picture', $picture);
}
}
if ($this->fieldProvider
->attributeIsSyncedOnEvent('[field.ldap_user_puid]', $event)) {
$ldap_user_puid = $this->server
->derivePuidFromLdapResponse($this->ldapEntry);
if (!empty($ldap_user_puid)) {
$this->account
->set('ldap_user_puid', $ldap_user_puid);
}
}
if ($this->fieldProvider
->attributeIsSyncedOnEvent('[field.ldap_user_puid_property]', $event)) {
$this->account
->set('ldap_user_puid_property', $this->server
->getUniquePersistentAttribute());
}
if ($this->fieldProvider
->attributeIsSyncedOnEvent('[field.ldap_user_puid_sid]', $event)) {
$this->account
->set('ldap_user_puid_sid', $this->server
->id());
}
if ($this->fieldProvider
->attributeIsSyncedOnEvent('[field.ldap_user_current_dn]', $event)) {
$this->account
->set('ldap_user_current_dn', $this->ldapEntry
->getDn());
}
}
private function setUserDefinedMappings(string $event) : void {
$mappings = $this->fieldProvider
->getConfigurableAttributesSyncedOnEvent($event);
foreach ($mappings as $key => $mapping) {
if ($mapping
->isBinary() && strpos($mapping
->getLdapAttribute(), ';') === FALSE) {
$mapping
->setLdapAttribute(str_replace(']', ';binary]', $mapping
->getLdapAttribute()));
}
$value = $this->tokenProcessor
->ldapEntryReplacementsForDrupalAccount($this->ldapEntry, $mapping
->getLdapAttribute());
[
$value_type,
$value_name,
] = $this
->parseUserAttributeNames($key);
if ($value_type === 'field' || $value_type === 'property') {
$this->account
->set($value_name, $value === '' ? NULL : $value);
}
}
}
private function parseUserAttributeNames(string $user_attribute_key) : array {
$type = '';
$name = '';
$user_attribute_key = trim($user_attribute_key, '[]');
$parts = explode('.', $user_attribute_key);
if ($parts !== FALSE) {
$type = $parts[0];
$name = $parts[1] ?? '';
if ($name) {
$name_parts = explode(':', $name);
if ($name_parts !== FALSE && isset($name_parts[1])) {
$name = $name_parts[0];
}
}
}
return [
$type,
$name,
];
}
public function reset() : void {
$this->account = NULL;
$this->ldapEntry = NULL;
}
}