mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/ldap_groups_sync.git
synced 2026-03-10 10:27:40 -03:00
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>
1532 lines
51 KiB
PHP
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';
|
|
}
|
|
|
|
}
|