Files
ldap_groups_sync/modules/ldap_departments_sync/src/LdapDepartmentsSync.php
Quintino A. G. Souza 7e338677a3 Move submódulos para modules/ seguindo convenção Drupal
ldap_departments_sync/ e ldap_research_groups_sync/ movidos para
modules/, padrão adotado por módulos contrib como Drupal Commerce.
Nenhum arquivo PHP ou YAML alterado — o Drupal descobre módulos
recursivamente pelo .info.yml independente do caminho.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 09:22:22 -03:00

1532 lines
51 KiB
PHP

<?php
namespace Drupal\ldap_departments_sync;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\ldap_servers\LdapBridgeInterface;
use Drupal\Core\Entity\EntityStorageInterface;
/**
* Service to synchronize departments from LDAP to groups.
*/
class LdapDepartmentsSync {
/**
* Entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Logger.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* LDAP Bridge.
*
* @var \Drupal\ldap_servers\LdapBridgeInterface
*/
protected $ldapBridge;
/**
* LDAP Query storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $ldapQueryStorage;
/**
* Flag indicating that an authorized LDAP sync is in progress.
*
* Set to TRUE before saving user entities during sync so that
* hook_user_presave() knows to allow changes to protected fields.
* External LDAP sync processes (e.g. ldap_user) can also set this flag
* to signal that their saves are authorized.
*
* @var bool
*/
public static bool $syncing = FALSE;
/**
* Returns whether an authorized LDAP sync is currently running.
*
* @return bool
* TRUE if a sync is in progress, FALSE otherwise.
*/
public static function isSyncing(): bool {
return self::$syncing;
}
/**
* Constructor.
*/
public function __construct(
EntityTypeManagerInterface $entity_type_manager,
ConfigFactoryInterface $config_factory,
LoggerChannelFactoryInterface $logger_factory,
LdapBridgeInterface $ldap_bridge
) {
$this->entityTypeManager = $entity_type_manager;
$this->configFactory = $config_factory;
$this->logger = $logger_factory->get('ldap_departments_sync');
$this->ldapBridge = $ldap_bridge;
// Check if entity type ldap_query_entity exists
if ($entity_type_manager->hasDefinition('ldap_query_entity')) {
$this->ldapQueryStorage = $entity_type_manager->getStorage('ldap_query_entity');
}
else {
$this->logger->warning('Entity type ldap_query_entity not found. Check if ldap_query module is properly installed.');
$this->ldapQueryStorage = NULL;
}
}
/**
* Synchronizes departments from LDAP to groups.
*/
public function syncDepartments() {
$this->logger->info('Starting departments synchronization using configured LDAP query.');
// Verifica configuração antes de iniciar
$config = $this->configFactory->get('ldap_departments_sync.settings');
$bundle_id = $this->getBundleId();
$this->logger->info('Configuração atual - Server: @server, Query: @query, Group Type: @bundle', [
'@server' => $config->get('ldap_server_id') ?: 'não configurado',
'@query' => $config->get('ldap_query_id') ?: 'não configurado',
'@bundle' => $bundle_id,
]);
try {
// Conecta ao LDAP e obtém departments usando query configurada
$departments = $this->fetchDepartmentsUsingQuery();
}
catch (\Exception $e) {
$this->logger->error('Erro durante fetchDepartmentsUsingQuery: @message', ['@message' => $e->getMessage()]);
throw $e;
}
if (empty($departments)) {
$this->logger->warning('Nenhum department encontrado no LDAP.');
return;
}
$this->logger->info('Iniciando sincronização de @count departments', ['@count' => count($departments)]);
// Obtém todos as entidades existentes
$entity_storage = $this->getEntityStorage();
$bundle_field = $this->getBundleField();
$existing_entities = $entity_storage->loadByProperties([$bundle_field => $bundle_id]);
// Cria um mapa de códigos existentes
$existing_codes = [];
foreach ($existing_entities as $entity) {
if ($entity->hasField('field_dept_code') && !$entity->get('field_dept_code')->isEmpty()) {
$code = $entity->get('field_dept_code')->value;
$existing_codes[$code] = $entity;
}
}
$created = 0;
$updated = 0;
// Processa cada department do LDAP
foreach ($departments as $dept_data) {
$code = $dept_data['code'];
$name = $dept_data['name'];
$acronym = $dept_data['acronym'];
$type = $dept_data['type'];
$phone = $dept_data['phone'];
$room = $dept_data['room'];
$mail = $dept_data['mail'];
// Campos extras (incluindo referências de usuário)
$extra_fields = [];
foreach ($dept_data as $field => $value) {
if (!in_array($field, ['code', 'name', 'acronym', 'type', 'phone', 'room', 'mail'])) {
$extra_fields[$field] = $value;
}
}
$this->logger->info('Processando department - Código: @code, Nome: @name, Sigla: @acronym', [
'@code' => $code,
'@name' => $name,
'@acronym' => $acronym,
]);
if (isset($existing_codes[$code])) {
// Atualiza entidade existente
$this->logger->info('Atualizando entidade existente com código: @code', ['@code' => $code]);
$entity = $existing_codes[$code];
$name_field = $this->getNameField();
// Set name/label field
if ($name_field === 'label') {
$entity->set('label', $name);
}
else {
$entity->setName($name);
}
if ($entity->hasField('field_dept_acronym')) {
$entity->set('field_dept_acronym', $acronym);
}
if ($entity->hasField('field_dept_type')) {
$entity->set('field_dept_type', $type);
}
if ($entity->hasField('field_dept_phone')) {
$entity->set('field_dept_phone', $phone);
}
if ($entity->hasField('field_dept_room')) {
$entity->set('field_dept_room', $room);
}
if ($entity->hasField('field_dept_mail')) {
$entity->set('field_dept_mail', $mail);
}
// Atualizar campos extras
foreach ($extra_fields as $field => $value) {
if ($entity->hasField($field)) {
$entity->set($field, $value);
$this->logger->debug('Campo @field atualizado com valor @value', [
'@field' => $field,
'@value' => $value ?? '',
]);
} else {
$this->logger->warning('Campo @field não existe na entidade', [
'@field' => $field,
]);
}
}
try {
$entity->save();
$updated++;
$this->logger->info('Entidade atualizada com sucesso: @code', ['@code' => $code]);
}
catch (\Exception $e) {
$this->logger->error('Erro ao atualizar entidade @code: @error', [
'@code' => $code,
'@error' => $e->getMessage(),
]);
}
}
else {
// Cria nova entidade
$this->logger->info('Criando nova entidade com código: @code', ['@code' => $code]);
try {
$bundle_field = $this->getBundleField();
$name_field = $this->getNameField();
$entity_data = [
$bundle_field => $bundle_id,
$name_field => $name,
'field_dept_code' => $code,
'field_dept_acronym' => $acronym,
'field_dept_type' => $type,
'field_dept_phone' => $phone,
'field_dept_room' => $room,
'field_dept_mail' => $mail,
'uid' => 1, // Admin user as owner
];
// Adicionar campos extras
foreach ($extra_fields as $field => $value) {
$entity_data[$field] = $value;
$this->logger->debug('Campo @field configurado com valor @value', [
'@field' => $field,
'@value' => $value ?? '',
]);
}
$entity = $entity_storage->create($entity_data);
$entity->save();
$created++;
$this->logger->info('Entidade criada com sucesso: @code (ID: @id)', [
'@code' => $code,
'@id' => $entity->id(),
]);
}
catch (\Exception $e) {
$this->logger->error('Erro ao criar entidade @code: @error', [
'@code' => $code,
'@error' => $e->getMessage(),
]);
}
}
}
if ($created > 0 || $updated > 0) {
$this->logger->info('Sincronização concluída com sucesso. Criados: @created, Atualizados: @updated', [
'@created' => $created,
'@updated' => $updated,
]);
}
else {
$this->logger->info('Nenhum departamento criado ou atualizado.');
}
// Aplica hierarquização se habilitada
$this->applyHierarchy();
// Atualiza campo field_user_department dos usuários locais
$this->syncUserDepartments();
// Sincroniza membros dos grupos
$this->syncGroupMembers();
}
/**
* Busca entidade de departamento pelo código.
*
* @param string $dept_code
* Código do departamento.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* Entidade (termo de taxonomia ou grupo) ou NULL se não encontrado.
*/
public function getDepartmentByCode($dept_code) {
if (empty($dept_code)) {
return NULL;
}
$bundle_id = $this->getBundleId();
$bundle_field = $this->getBundleField();
$entity_storage = $this->getEntityStorage();
$entities = $entity_storage->loadByProperties([
$bundle_field => $bundle_id,
'field_dept_code' => $dept_code,
]);
return !empty($entities) ? reset($entities) : NULL;
}
/**
* Busca departments usando a query LDAP configurada.
*
* @return array
* Array de departments com código e descrição.
*/
protected function fetchDepartmentsUsingQuery() {
$departments = [];
$this->logger->info('Iniciando fetchDepartmentsUsingQuery...');
// Verifica se o storage de queries está disponível
if (!$this->ldapQueryStorage) {
$this->logger->error('Storage de queries LDAP não está disponível. Verifique se o módulo ldap_query está instalado.');
return $departments;
}
$this->logger->info('Storage de queries LDAP está disponível.');
try {
// Obtém ID da query configurada
$config = $this->configFactory->get('ldap_departments_sync.settings');
$query_id = $config->get('ldap_query_id') ?: 'department_sync';
$this->logger->info('Tentando carregar query LDAP: @query_id', ['@query_id' => $query_id]);
// Carrega a query LDAP configurada
$query_entity = $this->ldapQueryStorage->load($query_id);
if (!$query_entity) {
$this->logger->error('Query LDAP "@query_id" não encontrada. Configure a query via /admin/config/local-modules/ldap-departments-sync', [
'@query_id' => $query_id,
]);
return $departments;
}
$this->logger->info('Query LDAP carregada com sucesso: @label', ['@label' => $query_entity->label()]);
if (!$query_entity->get('status')) {
$this->logger->error('Query LDAP "@query_id" está desabilitada.', [
'@query_id' => $query_id,
]);
return $departments;
}
$this->logger->info('Usando query LDAP configurada: @query_id (@label)', [
'@query_id' => $query_id,
'@label' => $query_entity->label(),
]);
$this->logger->info('Executando query LDAP...');
// Verifica métodos disponíveis na query entity
$methods = get_class_methods($query_entity);
$this->logger->info('Métodos disponíveis na query: @methods', ['@methods' => implode(', ', $methods)]);
// Usa os parâmetros da query com LdapBridge
try {
// Obtém parâmetros da query
$server_id = $query_entity->getServerId();
$base_dns = $query_entity->getProcessedBaseDns();
$filter = $query_entity->getFilter();
$attributes = $query_entity->getProcessedAttributes();
$this->logger->info('Parâmetros da query - Server: @server, Base DN: @base_dn, Filter: @filter, Attributes: @attrs', [
'@server' => $server_id,
'@base_dn' => !empty($base_dns) ? implode(', ', $base_dns) : 'vazio',
'@filter' => $filter,
'@attrs' => !empty($attributes) ? implode(', ', $attributes) : 'vazio',
]);
// Configura o LdapBridge com o servidor da query
$this->ldapBridge->setServerById($server_id);
// Conecta ao servidor
if (!$this->ldapBridge->bind()) {
throw new \Exception('Falha ao conectar ao servidor LDAP');
}
$this->logger->info('Conectado ao servidor LDAP com sucesso');
// Executa busca para cada Base DN
$all_results = [];
foreach ($base_dns as $base_dn) {
$this->logger->info('Executando busca em Base DN: @base_dn', ['@base_dn' => $base_dn]);
try {
$ldap = $this->ldapBridge->get();
$this->logger->info('Obtida instância LDAP do bridge');
// Prepara atributos (pode ser array ou string)
$attr_options = [];
if (!empty($attributes)) {
$attr_options = ['filter' => $attributes];
}
$this->logger->info('Criando query Symfony LDAP...');
$query = $ldap->query($base_dn, $filter, $attr_options);
$this->logger->info('Executando query Symfony LDAP...');
$base_results = $query->execute();
$this->logger->info('Query executada. Tipo de resultado: @type', ['@type' => gettype($base_results)]);
// Converte para array se necessário
if ($base_results instanceof \Traversable) {
$base_results = iterator_to_array($base_results);
}
$this->logger->info('Resultados encontrados: @count', ['@count' => count($base_results)]);
if (!empty($base_results) && is_array($base_results)) {
$all_results = array_merge($all_results, $base_results);
}
}
catch (\Exception $e) {
$this->logger->error('Erro na busca LDAP para Base DN @base_dn: @message', [
'@base_dn' => $base_dn,
'@message' => $e->getMessage(),
]);
// Continua para próximo Base DN se houver erro
}
}
$results = $all_results;
$this->logger->info('Query LDAP executada com sucesso. Total de resultados: @count', ['@count' => count($results)]);
}
catch (\Exception $e) {
$this->logger->error('Erro ao executar query LDAP: @message', ['@message' => $e->getMessage()]);
$this->logger->error('Stack trace: @trace', ['@trace' => $e->getTraceAsString()]);
throw $e;
}
$this->logger->info('Verificando resultados da query...');
if (empty($results)) {
$this->logger->warning('Query LDAP não retornou resultados.');
return $departments;
}
$this->logger->info('Query LDAP retornou @count resultados', ['@count' => count($results)]);
// Processa os resultados da query usando mapeamentos dinâmicos
$departments = $this->processLdapResults($results);
$this->logger->info('Processados @count departments via query LDAP', ['@count' => count($departments)]);
}
catch (\Exception $e) {
$this->logger->error('Erro ao executar query LDAP: @message', ['@message' => $e->getMessage()]);
}
return $departments;
}
/**
* Sincroniza departamentos dos usuários locais do Drupal.
*
* Este método atualiza o campo field_user_department dos usuários locais
* (criados pelo ldap_user) fazendo match entre field_user_dept_code e
* field_dept_code do departamento.
*/
public function syncUserDepartments() {
// Obtém configurações
$config = $this->configFactory->get('ldap_departments_sync.settings');
// Verifica se a sincronização de usuários está habilitada
if (!$config->get('sync_users')) {
$this->logger->info('User synchronization is disabled.');
return;
}
$this->logger->info('Starting user department field synchronization.');
// Carrega todos os usuários ativos que têm código de departamento
$user_storage = $this->entityTypeManager->getStorage('user');
$query = $user_storage->getQuery()
->condition('status', 1)
->condition('uid', 0, '>') // Exclui usuário anônimo
->exists('field_user_dept_code')
->accessCheck(FALSE);
$uids = $query->execute();
if (empty($uids)) {
$this->logger->warning('No users with field_user_dept_code found.');
return;
}
$users = $user_storage->loadMultiple($uids);
$this->logger->info('Processing @count users for department field update', ['@count' => count($users)]);
// Carrega todos os departamentos uma vez
$dept_storage = $this->getEntityStorage();
$bundle_field = $this->getBundleField();
$bundle_id = $this->getBundleId();
$departments = $dept_storage->loadByProperties([$bundle_field => $bundle_id]);
// Cria um mapa de dept_code => department para busca rápida
$depts_by_code = [];
foreach ($departments as $dept) {
if ($dept->hasField('field_dept_code') && !$dept->get('field_dept_code')->isEmpty()) {
$code = $dept->get('field_dept_code')->value;
$depts_by_code[$code] = $dept;
}
}
$this->logger->info('Found @count departments with codes', ['@count' => count($depts_by_code)]);
$updated = 0;
$skipped = 0;
// Sinaliza que os saves a seguir fazem parte de um sync LDAP autorizado,
// de modo que hook_user_presave() não bloqueie as alterações.
self::$syncing = TRUE;
try {
foreach ($users as $user) {
$username = $user->getAccountName();
// Obtém o código de departamento do usuário
if (!$user->hasField('field_user_dept_code') || $user->get('field_user_dept_code')->isEmpty()) {
$skipped++;
continue;
}
$user_dept_code = $user->get('field_user_dept_code')->value;
// Busca o departamento correspondente pelo código
if (!isset($depts_by_code[$user_dept_code])) {
$this->logger->warning('No department found with code "@code" for user @username. field_user_department will not be updated.', [
'@code' => $user_dept_code,
'@username' => $username,
]);
$skipped++;
continue;
}
$department = $depts_by_code[$user_dept_code];
// Atualiza campo field_user_department do usuário
if ($user->hasField('field_user_department')) {
$current_dept = $user->get('field_user_department')->target_id;
if ($current_dept != $department->id()) {
$user->set('field_user_department', $department->id());
$user->save();
$updated++;
} else {
$skipped++;
}
} else {
$this->logger->warning('User @username does not have field_user_department', [
'@username' => $username,
]);
$skipped++;
}
}
}
finally {
self::$syncing = FALSE;
}
$this->logger->info('User department field synchronization completed. Updated: @updated, Skipped: @skipped', [
'@updated' => $updated,
'@skipped' => $skipped,
]);
}
/**
* Sincroniza membros dos grupos baseado no código de departamento dos usuários.
*
* Este método adiciona usuários do Drupal como membros dos grupos fazendo
* match entre user.field_user_dept_code e group.field_dept_code.
*/
public function syncGroupMembers() {
$this->logger->info('Starting group membership synchronization.');
$config = $this->configFactory->get('ldap_departments_sync.settings');
$role_mapping_enabled = $config->get('role_mapping_enabled') ?? FALSE;
$role_mappings = $config->get('role_mappings') ?? [];
// Se role mapping não está habilitado, não faz sincronização
if (!$role_mapping_enabled) {
$this->logger->info('Role mapping is disabled. Skipping group membership synchronization.');
return;
}
if (empty($role_mappings)) {
$this->logger->warning('Role mapping is enabled but no mappings are configured. Skipping synchronization.');
return;
}
// Carrega todos os usuários ativos
$user_storage = $this->entityTypeManager->getStorage('user');
$query = $user_storage->getQuery()
->condition('status', 1)
->condition('uid', 0, '>') // Exclui usuário anônimo
->accessCheck(FALSE);
$uids = $query->execute();
if (empty($uids)) {
$this->logger->warning('No active users found.');
return;
}
$users = $user_storage->loadMultiple($uids);
$this->logger->info('Processing @count users for group membership', ['@count' => count($users)]);
// Carrega todos os grupos
$group_storage = $this->entityTypeManager->getStorage('group');
$group_type_id = $this->getBundleId();
$groups = $group_storage->loadByProperties(['type' => $group_type_id]);
$this->logger->info('Found @count groups', ['@count' => count($groups)]);
$added = 0;
$removed = 0;
$role_updated = 0;
$skipped = 0;
$already_member = 0;
$matched = 0;
// Para cada usuário, verifica em quais grupos ele deve estar
foreach ($users as $user) {
try {
$username = $user->getAccountName();
// Determina em quais grupos e com quais roles o usuário deve estar
$expected_memberships = []; // [group_id => role_id]
foreach ($groups as $group) {
$group_role = $this->determineUserGroupRole($user, $group, $role_mapping_enabled, $role_mappings);
// Se determinou um role, o usuário deve estar neste grupo
if ($group_role !== NULL) {
$expected_memberships[$group->id()] = $group_role;
}
}
if (!empty($expected_memberships)) {
$matched++;
}
// Processa as memberships esperadas
foreach ($expected_memberships as $group_id => $expected_role) {
$group = $groups[$group_id];
$membership = $group->getMember($user);
if ($membership) {
// Usuário já é membro, verifica se precisa atualizar o papel
$current_roles = $membership->getRoles();
$current_role_ids = array_map(function($role) {
return $role->id();
}, $current_roles);
// Se o papel esperado não está entre os papéis atuais, atualiza
if (!in_array($expected_role, $current_role_ids)) {
try {
// Remove papéis antigos (exceto 'member' base e o novo papel)
foreach ($current_role_ids as $role_id) {
if ($role_id !== 'member' && $role_id !== $expected_role) {
$membership->removeRole($role_id);
}
}
// Adiciona novo papel (se não for apenas 'member')
if ($expected_role !== 'member' && strpos($expected_role, '-member') === FALSE) {
$membership->addRole($expected_role);
}
$membership->save();
$role_updated++;
$this->logger->info('Updated role for user @username in group @group to @role', [
'@username' => $username,
'@group' => $group->label(),
'@role' => $expected_role,
]);
} catch (\Exception $e) {
$this->logger->error('Failed to update role for user @username in group @group: @error', [
'@username' => $username,
'@group' => $group->label(),
'@error' => $e->getMessage(),
]);
}
}
else {
$already_member++;
}
} else {
// Usuário não é membro, adiciona
try {
$values = [];
// Adiciona role se não for o member padrão
if ($expected_role !== 'member' && strpos($expected_role, '-member') === FALSE) {
$values['group_roles'] = [$expected_role];
}
$group->addMember($user, $values);
$added++;
$this->logger->info('Added user @username to group @group with role @role', [
'@username' => $username,
'@group' => $group->label(),
'@role' => $expected_role,
]);
} catch (\Exception $e) {
$this->logger->error('Failed to add user @username to group @group: @error', [
'@username' => $username,
'@group' => $group->label(),
'@error' => $e->getMessage(),
]);
$skipped++;
}
}
}
// Remove usuário de grupos onde ele não deveria estar mais
foreach ($groups as $group) {
if (!isset($expected_memberships[$group->id()])) {
$membership = $group->getMember($user);
if ($membership) {
try {
$group->removeMember($user);
$removed++;
$this->logger->info('Removed user @username from group @group', [
'@username' => $username,
'@group' => $group->label(),
]);
} catch (\Exception $e) {
$this->logger->error('Failed to remove user @username from group @group: @error', [
'@username' => $username,
'@group' => $group->label(),
'@error' => $e->getMessage(),
]);
}
}
}
}
} catch (\Exception $e) {
$this->logger->error('Error processing user @uid: @error', [
'@uid' => $user->id(),
'@error' => $e->getMessage(),
]);
$skipped++;
}
}
$this->logger->info('Group membership synchronization completed. Matched: @matched, Added: @added, Already member: @already, Removed: @removed, Role updated: @role_updated, Skipped: @skipped', [
'@matched' => $matched,
'@added' => $added,
'@already' => $already_member,
'@removed' => $removed,
'@role_updated' => $role_updated,
'@skipped' => $skipped,
]);
}
/**
* Determina o papel (role) de um usuário em um grupo baseado nos mapeamentos.
*
* @param \Drupal\user\UserInterface $user
* O usuário para verificar.
* @param \Drupal\group\Entity\GroupInterface $group
* O grupo onde o usuário será adicionado.
* @param bool $role_mapping_enabled
* Se o mapeamento de papéis está habilitado.
* @param array $role_mappings
* Array de mapeamentos de papéis.
*
* @return string|null
* O ID do papel a ser atribuído ao usuário, ou NULL se não houver match.
*/
protected function determineUserGroupRole($user, $group, $role_mapping_enabled, $role_mappings) {
// Se mapeamento de papéis não está habilitado, retorna NULL
if (!$role_mapping_enabled || empty($role_mappings)) {
return NULL;
}
// Itera pelos mapeamentos na ordem configurada
foreach ($role_mappings as $mapping) {
$group_role = $mapping['group_role'] ?? '';
$source = $mapping['source'] ?? 'user_field';
$source_field = $mapping['source_field'] ?? '';
$values = $mapping['values'] ?? [];
$group_field = $mapping['group_field'] ?? '';
if (empty($group_role) || empty($source_field)) {
continue;
}
$user_value = NULL;
// Obtém o valor do usuário baseado na fonte
if ($source === 'user_field') {
// Busca em campo do usuário do Drupal
if ($user->hasField($source_field) && !$user->get($source_field)->isEmpty()) {
$field = $user->get($source_field);
// Trata diferentes tipos de campo
$field_type = $field->getFieldDefinition()->getType();
if (in_array($field_type, ['entity_reference', 'entity_reference_revisions'])) {
// Para referências de entidade, pega o target_id
$user_value = $field->target_id;
} else {
// Para campos simples, pega o value
$user_value = $field->value;
}
}
} elseif ($source === 'ldap_attribute') {
// Para atributos LDAP, precisa buscar do LDAP
$config = $this->configFactory->get('ldap_departments_sync.settings');
$ldap_user_data = $this->fetchUserLdapAttribute($user, $source_field, $config);
if ($ldap_user_data !== NULL) {
$user_value = $ldap_user_data;
}
} elseif ($source === 'group_field_match') {
// Para group_field_match, compara campo do usuário com campo do grupo
if ($user->hasField($source_field) && !$user->get($source_field)->isEmpty() &&
$group->hasField($group_field) && !$group->get($group_field)->isEmpty()) {
$user_field = $user->get($source_field);
$group_field_obj = $group->get($group_field);
// Obtém valores
$user_field_type = $user_field->getFieldDefinition()->getType();
if (in_array($user_field_type, ['entity_reference', 'entity_reference_revisions'])) {
$user_value = $user_field->target_id;
} else {
$user_value = $user_field->value;
}
$group_field_type = $group_field_obj->getFieldDefinition()->getType();
if (in_array($group_field_type, ['entity_reference', 'entity_reference_revisions'])) {
$group_value = $group_field_obj->target_id;
} else {
$group_value = $group_field_obj->value;
}
// Compara valores (case-insensitive)
if ($user_value !== NULL && $group_value !== NULL &&
strcasecmp(trim($user_value), trim($group_value)) === 0) {
return $group_role;
}
}
continue; // Não há valores fixos para comparar, então pula para próximo mapping
}
// Verifica se o valor do usuário corresponde a algum dos valores fixos do mapeamento
if ($user_value !== NULL && !empty($values)) {
foreach ($values as $mapping_value) {
if (strcasecmp(trim($user_value), trim($mapping_value)) === 0) {
return $group_role;
}
}
}
}
// Se nenhum mapeamento corresponder, retorna NULL (usuário não deve estar neste grupo)
return NULL;
}
/**
* Busca um atributo LDAP específico para um usuário.
*
* @param \Drupal\user\UserInterface $user
* O usuário.
* @param string $attribute
* O atributo LDAP a buscar.
* @param \Drupal\Core\Config\ImmutableConfig $config
* Configuração do módulo.
*
* @return mixed|null
* O valor do atributo ou NULL se não encontrado.
*/
protected function fetchUserLdapAttribute($user, $attribute, $config) {
try {
// Obtém configurações LDAP
$host = $config->get('ldap_host');
$port = $config->get('ldap_port');
$base_dn = $config->get('users_base_dn') ?? $config->get('ldap_base_dn');
$bind_dn = $config->get('ldap_bind_dn');
$bind_password = $config->get('ldap_bind_password');
$users_filter = $config->get('users_filter') ?? '(objectClass=person)';
// Valida configurações mínimas
if (empty($host) || empty($base_dn)) {
return NULL;
}
// Conecta ao servidor LDAP
$ldap_connection = ldap_connect($host, $port);
if (!$ldap_connection) {
return NULL;
}
ldap_set_option($ldap_connection, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap_connection, LDAP_OPT_REFERRALS, 0);
// Faz bind
if (!empty($bind_dn) && !empty($bind_password)) {
$bind = @ldap_bind($ldap_connection, $bind_dn, $bind_password);
} else {
$bind = @ldap_bind($ldap_connection);
}
if (!$bind) {
ldap_close($ldap_connection);
return NULL;
}
// Busca o usuário
$username = $user->getAccountName();
$filter = "(&{$users_filter}(uid={$username}))";
$search = ldap_search($ldap_connection, $base_dn, $filter, [$attribute]);
if (!$search) {
ldap_close($ldap_connection);
return NULL;
}
$entries = ldap_get_entries($ldap_connection, $search);
ldap_close($ldap_connection);
// Retorna o valor do atributo se encontrado
if (!empty($entries) && $entries['count'] > 0 && isset($entries[0][$attribute])) {
return $entries[0][$attribute][0] ?? NULL;
}
return NULL;
} catch (\Exception $e) {
$this->logger->error('Error fetching LDAP attribute @attribute for user @username: @error', [
'@attribute' => $attribute,
'@username' => $user->getAccountName(),
'@error' => $e->getMessage(),
]);
return NULL;
}
}
/**
* Busca usuários do servidor LDAP.
*
* @param \Drupal\Core\Config\ImmutableConfig $config
* Configuração do módulo.
*
* @return array
* Array de usuários com username e código do departamento.
*/
protected function fetchUsersFromLdap($config) {
$users = [];
// Obtém configurações LDAP
$ldap_host = $config->get('ldap_host') ?: 'ldap://localhost';
$ldap_port = $config->get('ldap_port') ?: 389;
$users_base_dn = $config->get('users_base_dn') ?: 'ou=People,dc=example,dc=com';
$ldap_bind_dn = $config->get('ldap_bind_dn') ?: '';
$ldap_bind_password = $config->get('ldap_bind_password') ?: '';
$users_filter = $config->get('users_filter') ?: '(objectClass=person)';
$users_department_attribute = $config->get('users_department_attribute') ?: 'departmentNumber';
// Valida configurações essenciais
if (empty($ldap_host) || empty($users_base_dn)) {
$this->logger->error('Configurações LDAP para usuários incompletas. Host: @host, Base DN: @base_dn', [
'@host' => $ldap_host,
'@base_dn' => $users_base_dn,
]);
return $users;
}
// Verifica se a extensão LDAP está disponível
if (!function_exists('ldap_connect')) {
$this->logger->error('Extensão PHP LDAP não está instalada.');
return $users;
}
// Conecta ao servidor LDAP (reutiliza lógica existente)
$ldap_url = $ldap_host;
if (!parse_url($ldap_host, PHP_URL_PORT) && $ldap_port != 389) {
$scheme = parse_url($ldap_host, PHP_URL_SCHEME) ?: 'ldap';
$host = parse_url($ldap_host, PHP_URL_HOST) ?: str_replace(['ldap://', 'ldaps://'], '', $ldap_host);
$ldap_url = $scheme . '://' . $host . ':' . $ldap_port;
}
$this->logger->info('Conectando ao LDAP para usuários: @url', ['@url' => $ldap_url]);
$ldap_conn = ldap_connect($ldap_url);
if (!$ldap_conn) {
$this->logger->error('Não foi possível criar conexão LDAP para usuários: @url', ['@url' => $ldap_url]);
return $users;
}
// Define opções LDAP
ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap_conn, LDAP_OPT_REFERRALS, 0);
ldap_set_option($ldap_conn, LDAP_OPT_NETWORK_TIMEOUT, 10);
// Autentica no LDAP
if ($ldap_bind_dn && $ldap_bind_password) {
$ldap_bind = @ldap_bind($ldap_conn, $ldap_bind_dn, $ldap_bind_password);
}
else {
$ldap_bind = @ldap_bind($ldap_conn);
}
if (!$ldap_bind) {
$error = ldap_error($ldap_conn);
$errno = ldap_errno($ldap_conn);
$this->logger->error('Falha na autenticação LDAP para usuários: @error (Código: @errno)', [
'@error' => $error,
'@errno' => $errno,
]);
ldap_close($ldap_conn);
return $users;
}
// Executa busca LDAP
$this->logger->info('Executando busca LDAP para usuários - Base DN: @base_dn, Filtro: @filter', [
'@base_dn' => $users_base_dn,
'@filter' => $users_filter,
]);
// Define atributos específicos para retornar
$attributes = [
'uid',
'cn',
$users_department_attribute,
'dn'
];
$search_result = @ldap_search($ldap_conn, $users_base_dn, $users_filter, $attributes);
if (!$search_result) {
$error = ldap_error($ldap_conn);
$errno = ldap_errno($ldap_conn);
$this->logger->error('Erro na busca LDAP para usuários: @error (Código: @errno)', [
'@error' => $error,
'@errno' => $errno,
]);
ldap_close($ldap_conn);
return $users;
}
// Obtém entradas
$entries = ldap_get_entries($ldap_conn, $search_result);
$this->logger->info('Encontrados @count usuários no LDAP', ['@count' => $entries['count']]);
// Processa resultados
for ($i = 0; $i < $entries['count']; $i++) {
$entry = $entries[$i];
// Busca case-insensitive dos atributos
$username = '';
$department_code = '';
foreach ($entry as $attr_name => $attr_value) {
if (strcasecmp($attr_name, 'uid') === 0 && isset($attr_value[0])) {
$username = trim($attr_value[0]);
}
elseif (strcasecmp($attr_name, $users_department_attribute) === 0 && isset($attr_value[0])) {
$department_code = trim($attr_value[0]);
}
}
if (!empty($username) && !empty($department_code)) {
$users[] = [
'username' => $username,
'department_code' => $department_code,
];
$this->logger->debug('Usuário encontrado - Username: @username, Dept Code: @dept', [
'@username' => $username,
'@dept' => $department_code,
]);
}
}
// Fecha conexão
ldap_close($ldap_conn);
$this->logger->info('Processados @count usuários do LDAP', ['@count' => count($users)]);
return $users;
}
/**
* Processes LDAP results using configured attribute mappings.
*
* @param array $results
* Array of LDAP results.
*
* @return array
* Array of processed departments.
*/
protected function processLdapResults(array $results) {
$departments = [];
$config = $this->configFactory->get('ldap_departments_sync.settings');
$attribute_mappings = $config->get('attribute_mappings') ?: [];
// Get the code mapping to use as identifier
$code_mapping = null;
foreach ($attribute_mappings as $mapping) {
if ($mapping['field'] === 'field_dept_code') {
$code_mapping = $mapping;
break;
}
}
if (!$code_mapping) {
$this->logger->error('No field_dept_code mapping found. Cannot process results.');
return $departments;
}
$this->logger->debug('Processing @count LDAP results with @mappings mappings', [
'@count' => count($results),
'@mappings' => count($attribute_mappings),
]);
foreach ($results as $entry) {
$dept_data = [];
// Process each attribute mapping
foreach ($attribute_mappings as $mapping) {
$field = $mapping['field'];
$attribute = $mapping['attribute'];
$mapping_type = $mapping['mapping_type'] ?? 'simple';
// Get value from LDAP entry (Entry object)
$value = null;
if ($entry->hasAttribute($attribute)) {
$attr_values = $entry->getAttribute($attribute);
if (is_array($attr_values) && isset($attr_values[0])) {
$value = $attr_values[0];
} elseif (!empty($attr_values)) {
$value = $attr_values;
}
}
// Process based on mapping type
if ($mapping_type === 'user_reference') {
if (!empty($value)) {
// For user references, extract username from DN if needed
$username = $value;
if (stripos($value, 'uid=') !== FALSE) {
preg_match('/uid=([^,]+)/i', $value, $matches);
$username = $matches[1] ?? $value;
}
// Convert username to Drupal user ID
$user_id = $this->getUserIdByUsername($username);
if ($user_id) {
$dept_data[$field] = $user_id;
} else {
// User not found - this is normal if user doesn't exist in Drupal yet
// Log as debug instead of warning since this is expected behavior
$this->logger->debug('User @username not found in Drupal for field @field - reference will be empty', [
'@username' => $username,
'@field' => $field,
]);
$dept_data[$field] = null;
}
} else {
$dept_data[$field] = null;
}
} else {
$dept_data[$field] = $value;
}
}
// Use code as array key for backwards compatibility
$code = $dept_data['field_dept_code'] ?? null;
if ($code) {
// Map field names to legacy keys
$result = [
'code' => $code,
'name' => $dept_data['label'] ?? $dept_data['name'] ?? '',
'acronym' => $dept_data['field_dept_acronym'] ?? '',
'type' => $dept_data['field_dept_type'] ?? '',
'phone' => $dept_data['field_dept_phone'] ?? '',
'room' => $dept_data['field_dept_room'] ?? '',
'mail' => $dept_data['field_dept_mail'] ?? '',
];
// Include only extra mapped fields (not already covered above)
$basic_dept_fields = ['field_dept_code', 'label', 'name', 'field_dept_acronym', 'field_dept_type', 'field_dept_phone', 'field_dept_room', 'field_dept_mail'];
foreach ($dept_data as $key => $value) {
if (!in_array($key, $basic_dept_fields)) {
$result[$key] = $value;
}
}
$departments[] = $result;
}
}
return $departments;
}
/**
* Gets Drupal user ID by username.
*
* @param string $username
* The username to search for.
*
* @return int|null
* The user ID if found, NULL otherwise.
*/
protected function getUserIdByUsername($username) {
if (empty($username)) {
return null;
}
$user_storage = $this->entityTypeManager->getStorage('user');
$users = $user_storage->loadByProperties(['name' => $username]);
if (!empty($users)) {
$user = reset($users);
return $user->id();
}
return null;
}
/**
* Applies hierarchy to departments based on configuration.
*/
protected function applyHierarchy() {
$config = $this->configFactory->get('ldap_departments_sync.settings');
// Check if hierarchy is enabled
if (!$config->get('enable_hierarchy')) {
$this->logger->info('Hierarchy is disabled. Skipping hierarchy application.');
return;
}
$parent_attribute = $config->get('parent_attribute');
$child_attribute = $config->get('child_attribute');
if (empty($parent_attribute) || empty($child_attribute)) {
$this->logger->warning('Parent or child attribute not configured. Skipping hierarchy application.');
return;
}
$this->logger->info('Applying hierarchy for groups using parent attribute: @parent and child attribute: @child', [
'@parent' => $parent_attribute,
'@child' => $child_attribute,
]);
// Fetch raw LDAP entries to get hierarchy attributes
$ldap_entries = $this->fetchRawLdapEntries();
if (empty($ldap_entries)) {
$this->logger->warning('No LDAP entries found. Cannot apply hierarchy.');
return;
}
// Build hierarchy map from LDAP data
$hierarchy_map = $this->buildHierarchyMap($ldap_entries, $parent_attribute, $child_attribute);
$this->applyGroupHierarchy($hierarchy_map);
}
/**
* Fetches raw LDAP Entry objects from the query.
*
* @return array
* Array of Entry objects from LDAP.
*/
protected function fetchRawLdapEntries() {
$entries = [];
// Verifica se o storage de queries está disponível
if (!$this->ldapQueryStorage) {
$this->logger->error('Storage de queries LDAP não está disponível.');
return $entries;
}
try {
$config = $this->configFactory->get('ldap_departments_sync.settings');
$query_id = $config->get('ldap_query_id') ?: 'department_sync';
$query_entity = $this->ldapQueryStorage->load($query_id);
if (!$query_entity || !$query_entity->get('status')) {
$this->logger->error('Query LDAP não está disponível ou desabilitada.');
return $entries;
}
// Obtém parâmetros da query
$server_id = $query_entity->getServerId();
$base_dns = $query_entity->getProcessedBaseDns();
$filter = $query_entity->getFilter();
$attributes = $query_entity->getProcessedAttributes();
// Configura o LdapBridge
$this->ldapBridge->setServerById($server_id);
if (!$this->ldapBridge->bind()) {
throw new \Exception('Falha ao conectar ao servidor LDAP');
}
// Executa busca para cada Base DN
$all_results = [];
foreach ($base_dns as $base_dn) {
try {
$ldap = $this->ldapBridge->get();
$attr_options = [];
if (!empty($attributes)) {
$attr_options = ['filter' => $attributes];
}
$query = $ldap->query($base_dn, $filter, $attr_options);
$base_results = $query->execute();
if ($base_results instanceof \Traversable) {
$base_results = iterator_to_array($base_results);
}
if (!empty($base_results) && is_array($base_results)) {
$all_results = array_merge($all_results, $base_results);
}
}
catch (\Exception $e) {
$this->logger->error('Erro na busca LDAP para Base DN @base_dn: @message', [
'@base_dn' => $base_dn,
'@message' => $e->getMessage(),
]);
}
}
$entries = $all_results;
}
catch (\Exception $e) {
$this->logger->error('Erro ao buscar entries LDAP: @message', ['@message' => $e->getMessage()]);
}
return $entries;
}
/**
* Builds a hierarchy map from LDAP department data.
*
* @param array $ldap_entries
* Array of LDAP Entry objects.
* @param string $parent_attribute
* LDAP attribute containing parent reference.
* @param string $child_attribute
* LDAP attribute containing child reference (not used currently).
*
* @return array
* Map of child codes to parent codes.
*/
protected function buildHierarchyMap(array $ldap_entries, $parent_attribute, $child_attribute) {
$hierarchy_map = [];
$config = $this->configFactory->get('ldap_departments_sync.settings');
$attribute_mappings = $config->get('attribute_mappings') ?: [];
// Find the LDAP attribute that maps to field_dept_code
$code_attribute = null;
foreach ($attribute_mappings as $mapping) {
if ($mapping['field'] === 'field_dept_code') {
$code_attribute = $mapping['attribute'];
break;
}
}
if (!$code_attribute) {
$this->logger->error('No attribute mapping found for field_dept_code. Cannot build hierarchy.');
return $hierarchy_map;
}
foreach ($ldap_entries as $entry) {
// Get the department code from LDAP entry
$dept_code = null;
if ($entry->hasAttribute($code_attribute)) {
$code_values = $entry->getAttribute($code_attribute);
if (is_array($code_values) && isset($code_values[0])) {
$dept_code = $code_values[0];
} elseif (!empty($code_values)) {
$dept_code = $code_values;
}
}
if (!$dept_code) {
continue;
}
// Get parent reference from LDAP
$parent_value = null;
if ($entry->hasAttribute($parent_attribute)) {
$parent_values = $entry->getAttribute($parent_attribute);
if (is_array($parent_values) && isset($parent_values[0])) {
$parent_value = $parent_values[0];
} elseif (!empty($parent_values)) {
$parent_value = $parent_values;
}
}
// Extract parent code from DN or direct value
if (!empty($parent_value)) {
// If parent is a DN, extract the code
if (preg_match('/imeccDepartmentCode=([^,]+)/i', $parent_value, $matches)) {
$parent_code = $matches[1];
} else {
$parent_code = $parent_value;
}
$hierarchy_map[$dept_code] = $parent_code;
}
}
$this->logger->info('Built hierarchy map with @count relationships', [
'@count' => count($hierarchy_map),
]);
return $hierarchy_map;
}
/**
* Applies hierarchy to groups using custom field_parent_group field.
*
* @param array $hierarchy_map
* Map of child codes to parent codes.
*/
protected function applyGroupHierarchy(array $hierarchy_map) {
$storage = $this->getEntityStorage();
$updated = 0;
$this->logger->info('Applying group hierarchy using custom field_parent_group field.');
foreach ($hierarchy_map as $child_code => $parent_code) {
$child_group = $this->getDepartmentByCode($child_code);
$parent_group = $this->getDepartmentByCode($parent_code);
if ($child_group && $parent_group) {
// Check if child group has field_parent_group
if (!$child_group->hasField('field_parent_group')) {
$this->logger->warning('Group @child does not have field_parent_group. Please run database updates.', [
'@child' => $child_group->label(),
]);
continue;
}
// Check if parent is already set correctly
$current_parent_id = $child_group->get('field_parent_group')->target_id;
if ($current_parent_id != $parent_group->id()) {
try {
// Set the parent group
$child_group->set('field_parent_group', $parent_group->id());
$child_group->save();
$updated++;
$this->logger->info('Set parent group for @child to @parent', [
'@child' => $child_group->label(),
'@parent' => $parent_group->label(),
]);
} catch (\Exception $e) {
$this->logger->error('Failed to set parent for @child to @parent: @error', [
'@child' => $child_group->label(),
'@parent' => $parent_group->label(),
'@error' => $e->getMessage(),
]);
}
} else {
$this->logger->debug('Parent group already set for @child', [
'@child' => $child_group->label(),
]);
}
} else {
$this->logger->warning('Could not find groups for hierarchy relationship. Child code: @child_code, Parent code: @parent_code', [
'@child_code' => $child_code,
'@parent_code' => $parent_code,
]);
}
}
$this->logger->info('Group hierarchy applied. Updated @count group relationships.', [
'@count' => $updated,
]);
}
/**
* Returns the group entity storage.
*
* @return \Drupal\Core\Entity\EntityStorageInterface
* Entity storage for group.
*/
protected function getEntityStorage() {
return $this->entityTypeManager->getStorage('group');
}
/**
* Returns the group type ID from configuration.
*
* @return string
* The group type ID.
*/
protected function getBundleId() {
$config = $this->configFactory->get('ldap_departments_sync.settings');
return $config->get('group_type_id') ?: 'departments';
}
/**
* Returns the name field for groups.
*
* @return string
* Always returns 'label' for groups.
*/
protected function getNameField() {
return 'label';
}
/**
* Returns the bundle field name for groups.
*
* @return string
* Always returns 'type' for groups.
*/
protected function getBundleField() {
return 'type';
}
}