mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/ldap_groups_sync.git
synced 2026-03-09 18:07:41 -03:00
Cria o módulo modules/ldap_courses_sync/ seguindo o mesmo padrão do ldap_research_groups_sync, com bundle `course`, campos field_course_* e field_user_courses, sincronização de membros via atributo LDAP, e aba "Courses Sync" no painel unificado. Também registra `courses` no módulo pai (routing, UnifiedAccessRulesForm e GlobalAccessRuleForm) para suporte a access rules unificadas. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1143 lines
37 KiB
PHP
1143 lines
37 KiB
PHP
<?php
|
|
|
|
namespace Drupal\ldap_courses_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 courses from LDAP to groups.
|
|
*/
|
|
class LdapCoursesSync {
|
|
|
|
/**
|
|
* 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_courses_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 courses from LDAP to groups.
|
|
*/
|
|
public function syncCourses() {
|
|
$this->logger->info('Starting courses synchronization using configured LDAP query.');
|
|
|
|
$config = $this->configFactory->get('ldap_courses_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 {
|
|
$courses = $this->fetchCoursesUsingQuery();
|
|
}
|
|
catch (\Exception $e) {
|
|
$this->logger->error('Erro durante fetchCoursesUsingQuery: @message', ['@message' => $e->getMessage()]);
|
|
throw $e;
|
|
}
|
|
|
|
if (empty($courses)) {
|
|
$this->logger->warning('Nenhum curso encontrado no LDAP.');
|
|
return;
|
|
}
|
|
|
|
$this->logger->info('Iniciando sincronização de @count cursos', ['@count' => count($courses)]);
|
|
|
|
$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_course_code') && !$entity->get('field_course_code')->isEmpty()) {
|
|
$code = $entity->get('field_course_code')->value;
|
|
$existing_codes[$code] = $entity;
|
|
}
|
|
}
|
|
|
|
$created = 0;
|
|
$updated = 0;
|
|
|
|
foreach ($courses as $course_data) {
|
|
$code = $course_data['code'];
|
|
$name = $course_data['name'];
|
|
$phone = $course_data['phone'];
|
|
$mail = $course_data['mail'];
|
|
|
|
// Campos extras (incluindo referências de usuário)
|
|
$extra_fields = [];
|
|
foreach ($course_data as $field => $value) {
|
|
if (!in_array($field, ['code', 'name', 'phone', 'mail', '_ldap_members'])) {
|
|
$extra_fields[$field] = $value;
|
|
}
|
|
}
|
|
|
|
$this->logger->info('Processando curso - Código: @code, Nome: @name', [
|
|
'@code' => $code,
|
|
'@name' => $name,
|
|
]);
|
|
|
|
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();
|
|
|
|
if ($name_field === 'label') {
|
|
$entity->set('label', $name);
|
|
}
|
|
else {
|
|
$entity->setName($name);
|
|
}
|
|
|
|
if ($entity->hasField('field_course_phone')) {
|
|
$entity->set('field_course_phone', $phone);
|
|
}
|
|
if ($entity->hasField('field_course_mail')) {
|
|
$entity->set('field_course_mail', $mail);
|
|
}
|
|
|
|
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_course_code' => $code,
|
|
'field_course_phone' => $phone,
|
|
'field_course_mail' => $mail,
|
|
'uid' => 1,
|
|
];
|
|
|
|
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 curso criado ou atualizado.');
|
|
}
|
|
|
|
// Sincroniza membros dos grupos via atributo LDAP
|
|
$this->syncMembersFromLdapAttribute($courses);
|
|
}
|
|
|
|
/**
|
|
* Busca entidade de curso pelo código.
|
|
*
|
|
* @param string $course_code
|
|
* Código do curso.
|
|
*
|
|
* @return \Drupal\Core\Entity\EntityInterface|null
|
|
* Entidade grupo ou NULL se não encontrado.
|
|
*/
|
|
public function getCourseByCode($course_code) {
|
|
if (empty($course_code)) {
|
|
return NULL;
|
|
}
|
|
|
|
$bundle_id = $this->getBundleId();
|
|
$bundle_field = $this->getBundleField();
|
|
$entity_storage = $this->getEntityStorage();
|
|
|
|
$entities = $entity_storage->loadByProperties([
|
|
$bundle_field => $bundle_id,
|
|
'field_course_code' => $course_code,
|
|
]);
|
|
|
|
return !empty($entities) ? reset($entities) : NULL;
|
|
}
|
|
|
|
/**
|
|
* Busca cursos usando a query LDAP configurada.
|
|
*
|
|
* @return array
|
|
* Array de cursos com código e descrição.
|
|
*/
|
|
protected function fetchCoursesUsingQuery() {
|
|
$courses = [];
|
|
|
|
$this->logger->info('Iniciando fetchCoursesUsingQuery...');
|
|
|
|
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 $courses;
|
|
}
|
|
|
|
$this->logger->info('Storage de queries LDAP está disponível.');
|
|
|
|
try {
|
|
$config = $this->configFactory->get('ldap_courses_sync.settings');
|
|
$query_id = $config->get('ldap_query_id') ?: 'course_sync';
|
|
|
|
$this->logger->info('Tentando carregar query LDAP: @query_id', ['@query_id' => $query_id]);
|
|
|
|
$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-courses-sync', [
|
|
'@query_id' => $query_id,
|
|
]);
|
|
return $courses;
|
|
}
|
|
|
|
$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 $courses;
|
|
}
|
|
|
|
$this->logger->info('Usando query LDAP configurada: @query_id (@label)', [
|
|
'@query_id' => $query_id,
|
|
'@label' => $query_entity->label(),
|
|
]);
|
|
|
|
$this->logger->info('Executando query LDAP...');
|
|
|
|
try {
|
|
$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',
|
|
]);
|
|
|
|
$this->ldapBridge->setServerById($server_id);
|
|
|
|
if (!$this->ldapBridge->bind()) {
|
|
throw new \Exception('Falha ao conectar ao servidor LDAP');
|
|
}
|
|
|
|
$this->logger->info('Conectado ao servidor LDAP com sucesso');
|
|
|
|
$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');
|
|
|
|
// Garante que o atributo de membros está incluído na query
|
|
$member_sync_on = $config->get('member_sync_enabled') ?? TRUE;
|
|
if ($member_sync_on) {
|
|
$member_attr = $config->get('member_attribute') ?: 'member';
|
|
if (!empty($attributes) && !in_array($member_attr, $attributes)) {
|
|
$attributes[] = $member_attr;
|
|
}
|
|
}
|
|
|
|
$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)]);
|
|
|
|
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(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
$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 $courses;
|
|
}
|
|
|
|
$this->logger->info('Query LDAP retornou @count resultados', ['@count' => count($results)]);
|
|
|
|
$courses = $this->processLdapResults($results);
|
|
|
|
$this->logger->info('Processados @count cursos via query LDAP', ['@count' => count($courses)]);
|
|
}
|
|
catch (\Exception $e) {
|
|
$this->logger->error('Erro ao executar query LDAP: @message', ['@message' => $e->getMessage()]);
|
|
}
|
|
|
|
return $courses;
|
|
}
|
|
|
|
/**
|
|
* Sincroniza membros dos grupos baseado no código de grupo dos usuários.
|
|
*/
|
|
public function syncGroupMembers() {
|
|
$this->logger->info('Starting group membership synchronization.');
|
|
|
|
$config = $this->configFactory->get('ldap_courses_sync.settings');
|
|
$role_mapping_enabled = $config->get('role_mapping_enabled') ?? FALSE;
|
|
$role_mappings = $config->get('role_mappings') ?? [];
|
|
|
|
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;
|
|
}
|
|
|
|
$user_storage = $this->entityTypeManager->getStorage('user');
|
|
$query = $user_storage->getQuery()
|
|
->condition('status', 1)
|
|
->condition('uid', 0, '>')
|
|
->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)]);
|
|
|
|
$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;
|
|
|
|
foreach ($users as $user) {
|
|
try {
|
|
$username = $user->getAccountName();
|
|
$expected_memberships = [];
|
|
|
|
foreach ($groups as $group) {
|
|
$group_role = $this->determineUserGroupRole($user, $group, $role_mapping_enabled, $role_mappings);
|
|
|
|
if ($group_role !== NULL) {
|
|
$expected_memberships[$group->id()] = $group_role;
|
|
}
|
|
}
|
|
|
|
if (!empty($expected_memberships)) {
|
|
$matched++;
|
|
}
|
|
|
|
foreach ($expected_memberships as $group_id => $expected_role) {
|
|
$group = $groups[$group_id];
|
|
$membership = $group->getMember($user);
|
|
|
|
if ($membership) {
|
|
$current_roles = $membership->getRoles();
|
|
$current_role_ids = array_map(function ($role) {
|
|
return $role->id();
|
|
}, $current_roles);
|
|
|
|
if (!in_array($expected_role, $current_role_ids)) {
|
|
try {
|
|
foreach ($current_role_ids as $role_id) {
|
|
if ($role_id !== 'member' && $role_id !== $expected_role) {
|
|
$membership->removeRole($role_id);
|
|
}
|
|
}
|
|
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 {
|
|
try {
|
|
$values = [];
|
|
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++;
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Sincroniza membros dos cursos usando o atributo member do LDAP.
|
|
*
|
|
* Para cada curso, lê a lista de DNs do atributo configurado
|
|
* (padrão: 'member'), resolve cada DN para um usuário Drupal e gerencia
|
|
* as associações de membros do grupo (adiciona e remove conforme necessário).
|
|
*
|
|
* @param array $courses
|
|
* Array de dados dos cursos processados, com chave '_ldap_members'.
|
|
*/
|
|
public function syncMembersFromLdapAttribute(array $courses) {
|
|
$config = $this->configFactory->get('ldap_courses_sync.settings');
|
|
|
|
if (!($config->get('member_sync_enabled') ?? TRUE)) {
|
|
$this->logger->info('Member sync from LDAP attribute is disabled.');
|
|
return;
|
|
}
|
|
|
|
$this->logger->info('Starting member synchronization from LDAP attribute.');
|
|
|
|
$group_storage = $this->entityTypeManager->getStorage('group');
|
|
$user_storage = $this->entityTypeManager->getStorage('user');
|
|
$bundle_id = $this->getBundleId();
|
|
$bundle_field = $this->getBundleField();
|
|
|
|
$total_added = 0;
|
|
$total_removed = 0;
|
|
$total_not_found = 0;
|
|
|
|
foreach ($courses as $course_data) {
|
|
$code = $course_data['code'];
|
|
$ldap_members = $course_data['_ldap_members'] ?? [];
|
|
|
|
if (empty($ldap_members)) {
|
|
$this->logger->debug('No LDAP members for course @code.', ['@code' => $code]);
|
|
}
|
|
|
|
// Localiza a entidade grupo correspondente
|
|
$groups = $group_storage->loadByProperties([
|
|
$bundle_field => $bundle_id,
|
|
'field_course_code' => $code,
|
|
]);
|
|
|
|
if (empty($groups)) {
|
|
$this->logger->warning('Course with code @code not found for member sync.', ['@code' => $code]);
|
|
continue;
|
|
}
|
|
|
|
$group = reset($groups);
|
|
|
|
// Resolve DNs/UIDs LDAP para IDs de usuários Drupal
|
|
$expected_uids = [];
|
|
foreach ($ldap_members as $member_value) {
|
|
$username = $member_value;
|
|
// Se parece com um DN (contém '='), extrai o uid
|
|
if (strpos($member_value, '=') !== FALSE) {
|
|
if (preg_match('/uid=([^,]+)/i', $member_value, $matches)) {
|
|
$username = $matches[1];
|
|
}
|
|
else {
|
|
$this->logger->debug('Could not extract uid from DN: @dn', ['@dn' => $member_value]);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$uid = $this->getUserIdByUsername($username);
|
|
if ($uid) {
|
|
$expected_uids[$uid] = TRUE;
|
|
}
|
|
else {
|
|
$this->logger->debug('User @username (from @dn) not found in Drupal.', [
|
|
'@username' => $username,
|
|
'@dn' => $member_value,
|
|
]);
|
|
$total_not_found++;
|
|
}
|
|
}
|
|
|
|
// Obtém membros atuais do grupo
|
|
$current_memberships = $group->getMembers();
|
|
$current_uids = [];
|
|
foreach ($current_memberships as $membership) {
|
|
$member_entity = $membership->getUser();
|
|
if ($member_entity) {
|
|
$current_uids[$member_entity->id()] = TRUE;
|
|
}
|
|
}
|
|
|
|
// Adiciona novos membros
|
|
foreach (array_keys($expected_uids) as $uid) {
|
|
if (!isset($current_uids[$uid])) {
|
|
$user = $user_storage->load($uid);
|
|
if ($user) {
|
|
try {
|
|
$group->addMember($user);
|
|
$total_added++;
|
|
$this->logger->debug('Added user @uid to course @group.', [
|
|
'@uid' => $uid,
|
|
'@group' => $group->label(),
|
|
]);
|
|
// Append group to field_user_courses if not already there.
|
|
if ($user->hasField('field_user_courses')) {
|
|
$existing = $user->get('field_user_courses')->getValue();
|
|
$already_set = FALSE;
|
|
foreach ($existing as $ref) {
|
|
if ($ref['target_id'] == $group->id()) {
|
|
$already_set = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
if (!$already_set) {
|
|
self::$syncing = TRUE;
|
|
try {
|
|
$user->get('field_user_courses')->appendItem(['target_id' => $group->id()]);
|
|
$user->save();
|
|
}
|
|
finally {
|
|
self::$syncing = FALSE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (\Exception $e) {
|
|
$this->logger->error('Failed to add user @uid to course @group: @error', [
|
|
'@uid' => $uid,
|
|
'@group' => $group->label(),
|
|
'@error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove membros que não estão mais no LDAP
|
|
foreach (array_keys($current_uids) as $uid) {
|
|
if (!isset($expected_uids[$uid])) {
|
|
$user = $user_storage->load($uid);
|
|
if ($user) {
|
|
try {
|
|
$group->removeMember($user);
|
|
$total_removed++;
|
|
$this->logger->debug('Removed user @uid from course @group.', [
|
|
'@uid' => $uid,
|
|
'@group' => $group->label(),
|
|
]);
|
|
// Remove group from field_user_courses if present.
|
|
if ($user->hasField('field_user_courses')) {
|
|
$existing = $user->get('field_user_courses')->getValue();
|
|
$updated = array_values(array_filter($existing, fn($ref) => $ref['target_id'] != $group->id()));
|
|
if (count($updated) !== count($existing)) {
|
|
self::$syncing = TRUE;
|
|
try {
|
|
$user->set('field_user_courses', $updated);
|
|
$user->save();
|
|
}
|
|
finally {
|
|
self::$syncing = FALSE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (\Exception $e) {
|
|
$this->logger->error('Failed to remove user @uid from course @group: @error', [
|
|
'@uid' => $uid,
|
|
'@group' => $group->label(),
|
|
'@error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->logger->info('Member sync completed. Added: @added, Removed: @removed, Not found in Drupal: @not_found', [
|
|
'@added' => $total_added,
|
|
'@removed' => $total_removed,
|
|
'@not_found' => $total_not_found,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Determina o papel (role) de um usuário em um grupo baseado nos mapeamentos.
|
|
*/
|
|
protected function determineUserGroupRole($user, $group, $role_mapping_enabled, $role_mappings) {
|
|
if (!$role_mapping_enabled || empty($role_mappings)) {
|
|
return NULL;
|
|
}
|
|
|
|
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;
|
|
|
|
if ($source === 'user_field') {
|
|
if ($user->hasField($source_field) && !$user->get($source_field)->isEmpty()) {
|
|
$field = $user->get($source_field);
|
|
$field_type = $field->getFieldDefinition()->getType();
|
|
if (in_array($field_type, ['entity_reference', 'entity_reference_revisions'])) {
|
|
$user_value = $field->target_id;
|
|
}
|
|
else {
|
|
$user_value = $field->value;
|
|
}
|
|
}
|
|
}
|
|
elseif ($source === 'ldap_attribute') {
|
|
$config = $this->configFactory->get('ldap_courses_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') {
|
|
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);
|
|
|
|
$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;
|
|
}
|
|
|
|
if ($user_value !== NULL && $group_value !== NULL &&
|
|
strcasecmp(trim($user_value), trim($group_value)) === 0) {
|
|
return $group_role;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($user_value !== NULL && !empty($values)) {
|
|
foreach ($values as $mapping_value) {
|
|
if (strcasecmp(trim($user_value), trim($mapping_value)) === 0) {
|
|
return $group_role;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Busca um atributo LDAP específico para um usuário.
|
|
*/
|
|
protected function fetchUserLdapAttribute($user, $attribute, $config) {
|
|
try {
|
|
$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)';
|
|
|
|
if (empty($host) || empty($base_dn)) {
|
|
return NULL;
|
|
}
|
|
|
|
$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);
|
|
|
|
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;
|
|
}
|
|
|
|
$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);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes LDAP results using configured attribute mappings.
|
|
*
|
|
* @param array $results
|
|
* Array of LDAP results.
|
|
*
|
|
* @return array
|
|
* Array of processed courses.
|
|
*/
|
|
protected function processLdapResults(array $results) {
|
|
$courses = [];
|
|
$config = $this->configFactory->get('ldap_courses_sync.settings');
|
|
$attribute_mappings = $config->get('attribute_mappings') ?: [];
|
|
|
|
$code_mapping = NULL;
|
|
foreach ($attribute_mappings as $mapping) {
|
|
if ($mapping['field'] === 'field_course_code') {
|
|
$code_mapping = $mapping;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$code_mapping) {
|
|
$this->logger->error('No field_course_code mapping found. Cannot process results.');
|
|
return $courses;
|
|
}
|
|
|
|
$this->logger->debug('Processing @count LDAP results with @mappings mappings', [
|
|
'@count' => count($results),
|
|
'@mappings' => count($attribute_mappings),
|
|
]);
|
|
|
|
foreach ($results as $entry) {
|
|
$course_data = [];
|
|
|
|
foreach ($attribute_mappings as $mapping) {
|
|
$field = $mapping['field'];
|
|
$attribute = $mapping['attribute'];
|
|
$mapping_type = $mapping['mapping_type'] ?? 'simple';
|
|
|
|
$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;
|
|
}
|
|
}
|
|
elseif ($mapping_type === 'user_reference') {
|
|
$this->logger->warning('Atributo LDAP "@attribute" não encontrado na entrada para o campo "@field". Verifique se o atributo está incluído na LDAP query.', [
|
|
'@attribute' => $attribute,
|
|
'@field' => $field,
|
|
]);
|
|
}
|
|
|
|
if ($mapping_type === 'department_reference') {
|
|
if (!empty($value)) {
|
|
$dept_id = $this->getDepartmentIdByCode($value);
|
|
$course_data[$field] = $dept_id;
|
|
}
|
|
else {
|
|
$course_data[$field] = NULL;
|
|
}
|
|
}
|
|
elseif ($mapping_type === 'user_reference') {
|
|
if (!empty($value)) {
|
|
$username = $value;
|
|
// Se parece com DN (contém '='), tenta extrair o uid= ou o primeiro RDN.
|
|
if (strpos($value, '=') !== FALSE) {
|
|
if (preg_match('/uid=([^,]+)/i', $value, $matches)) {
|
|
$username = $matches[1];
|
|
}
|
|
else {
|
|
// Extrai o valor do primeiro RDN (ex.: cn=João Silva,... → João Silva)
|
|
preg_match('/^[^=]+=([^,]+)/i', $value, $matches);
|
|
$username = $matches[1] ?? $value;
|
|
$this->logger->warning('Campo "@field": DN sem uid= encontrado ("@dn"). Usando primeiro RDN "@username" como username.', [
|
|
'@field' => $field,
|
|
'@dn' => $value,
|
|
'@username' => $username,
|
|
]);
|
|
}
|
|
}
|
|
|
|
$user_id = $this->getUserIdByUsername($username);
|
|
|
|
if ($user_id) {
|
|
$course_data[$field] = $user_id;
|
|
}
|
|
else {
|
|
$this->logger->warning('Campo "@field": usuário "@username" (valor LDAP: "@value") não encontrado no Drupal. Campo ficará vazio.', [
|
|
'@field' => $field,
|
|
'@username' => $username,
|
|
'@value' => $value,
|
|
]);
|
|
$course_data[$field] = NULL;
|
|
}
|
|
}
|
|
else {
|
|
$course_data[$field] = NULL;
|
|
}
|
|
}
|
|
else {
|
|
$course_data[$field] = $value;
|
|
}
|
|
}
|
|
|
|
$code = $course_data['field_course_code'] ?? NULL;
|
|
if ($code) {
|
|
$result = [
|
|
'code' => $code,
|
|
'name' => $course_data['label'] ?? $course_data['name'] ?? '',
|
|
'phone' => $course_data['field_course_phone'] ?? '',
|
|
'mail' => $course_data['field_course_mail'] ?? '',
|
|
];
|
|
|
|
// Captura lista de membros do atributo LDAP configurado
|
|
$member_attribute = $config->get('member_attribute') ?: 'member';
|
|
if ($entry->hasAttribute($member_attribute)) {
|
|
$result['_ldap_members'] = $entry->getAttribute($member_attribute) ?? [];
|
|
}
|
|
else {
|
|
$result['_ldap_members'] = [];
|
|
}
|
|
|
|
$basic_course_fields = ['field_course_code', 'label', 'name', 'field_course_phone', 'field_course_mail'];
|
|
foreach ($course_data as $key => $value) {
|
|
if (!in_array($key, $basic_course_fields)) {
|
|
$result[$key] = $value;
|
|
}
|
|
}
|
|
|
|
$courses[] = $result;
|
|
}
|
|
}
|
|
|
|
return $courses;
|
|
}
|
|
|
|
/**
|
|
* Gets Drupal user ID by username.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Returns the Drupal group ID for a department with the given code.
|
|
*/
|
|
protected function getDepartmentIdByCode(string $code): ?int {
|
|
if (empty($code)) {
|
|
return NULL;
|
|
}
|
|
$groups = $this->entityTypeManager->getStorage('group')
|
|
->loadByProperties(['field_dept_code' => $code]);
|
|
if (!empty($groups)) {
|
|
return (int) reset($groups)->id();
|
|
}
|
|
$this->logger->debug('Department with field_dept_code @code not found.', ['@code' => $code]);
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Returns the group entity storage.
|
|
*/
|
|
protected function getEntityStorage() {
|
|
return $this->entityTypeManager->getStorage('group');
|
|
}
|
|
|
|
/**
|
|
* Returns the group type ID from configuration.
|
|
*/
|
|
protected function getBundleId() {
|
|
$config = $this->configFactory->get('ldap_courses_sync.settings');
|
|
return $config->get('group_type_id') ?: 'course';
|
|
}
|
|
|
|
/**
|
|
* Returns the name field for groups.
|
|
*/
|
|
protected function getNameField() {
|
|
return 'label';
|
|
}
|
|
|
|
/**
|
|
* Returns the bundle field name for groups.
|
|
*/
|
|
protected function getBundleField() {
|
|
return 'type';
|
|
}
|
|
|
|
}
|