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'; } }