Files
ldap_groups_sync/modules/ldap_courses_sync/ldap_courses_sync.module
Quintino A. G. Souza 4a65619c27 Adiciona submódulo ldap_courses_sync para sincronização de cursos via LDAP
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>
2026-03-02 13:49:47 -03:00

237 lines
7.9 KiB
Plaintext

<?php
/**
* @file
* Module to synchronize courses from LDAP to groups.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\field\FieldConfigInterface;
use Drupal\ldap_courses_sync\LdapCoursesSync;
/**
* Implements hook_help().
*/
function ldap_courses_sync_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.ldap_courses_sync':
return '<p>' . t('This module synchronizes courses from an LDAP server to groups through cron.') . '</p>';
}
}
/**
* Implements hook_cron().
*/
function ldap_courses_sync_cron() {
// Check if module is properly configured
$config = \Drupal::config('ldap_courses_sync.settings');
$ldap_server_id = $config->get('ldap_server_id');
$ldap_query_id = $config->get('ldap_query_id');
// Validate LDAP server
try {
$entity_type_manager = \Drupal::entityTypeManager();
$server_storage = $entity_type_manager->getStorage('ldap_server');
$server = $server_storage->load($ldap_server_id);
if (!$server || !$server->get('status')) {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: LDAP server "@server_id" not found or inactive. Configure at /admin/config/local-modules/ldap-courses-sync', [
'@server_id' => $ldap_server_id,
]);
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: error checking LDAP server: @message', [
'@message' => $e->getMessage(),
]);
return;
}
// Validate LDAP query
try {
if ($entity_type_manager->hasDefinition('ldap_query_entity')) {
$query_storage = $entity_type_manager->getStorage('ldap_query_entity');
$query = $query_storage->load($ldap_query_id);
if (!$query || !$query->get('status')) {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: LDAP query "@query_id" not found or inactive. Configure at /admin/config/local-modules/ldap-courses-sync', [
'@query_id' => $ldap_query_id,
]);
return;
}
}
else {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: ldap_query module not available.');
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: error checking LDAP query: @message', [
'@message' => $e->getMessage(),
]);
return;
}
// Validate group type
try {
$group_type_id = $config->get('group_type_id');
$group_type_storage = $entity_type_manager->getStorage('group_type');
$group_type = $group_type_storage->load($group_type_id);
if (!$group_type) {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: group type "@type_id" not found. Configure at /admin/config/local-modules/ldap-courses-sync', [
'@type_id' => $group_type_id,
]);
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: error checking group type: @message', [
'@message' => $e->getMessage(),
]);
return;
}
// Get LDAP synchronization service
$ldap_sync = \Drupal::service('ldap_courses_sync.sync');
// Execute courses synchronization
try {
$ldap_sync->syncCourses();
\Drupal::logger('ldap_courses_sync')->info('Courses synchronization executed successfully.');
}
catch (\Exception $e) {
\Drupal::logger('ldap_courses_sync')->error('Error in courses synchronization: @message', [
'@message' => $e->getMessage(),
]);
}
}
/**
* Implements hook_entity_access().
*
* Evaluates access rules configured in the module settings for view, update
* and delete operations on existing entities.
*/
function ldap_courses_sync_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
// Skip group-related entities to avoid conflicts with the group module.
$skip_types = ['group', 'group_content', 'group_relationship', 'group_role', 'group_type'];
if (in_array($entity->getEntityTypeId(), $skip_types, TRUE)) {
return AccessResult::neutral();
}
return \Drupal::service('ldap_courses_sync.access_rules')
->checkAccess($entity, $operation, $account);
}
/**
* Implements hook_entity_create_access().
*
* Evaluates access rules configured in the module settings for create
* operations on new entities.
*/
function ldap_courses_sync_entity_create_access(AccountInterface $account, array $context, $entity_bundle) {
$entity_type_id = $context['entity_type_id'] ?? '';
// Skip group-related entities.
$skip_types = ['group', 'group_content', 'group_relationship', 'group_role', 'group_type'];
if (in_array($entity_type_id, $skip_types, TRUE)) {
return AccessResult::neutral();
}
return \Drupal::service('ldap_courses_sync.access_rules')
->checkCreateAccess($account, $entity_type_id, $entity_bundle ?? '');
}
/**
* Implements hook_entity_field_access().
*
* Denies edit access to fields managed by the LDAP sync in forms and APIs
* (REST, JSON:API). Programmatic saves are not affected.
*
* Protected user fields:
* - field_user_courses: multi-value reference to the courses the user belongs
* to, populated by our sync. Protecting this field prevents the validation
* error "This entity cannot be referenced" that occurs when the widget
* tries to validate the referenced group against the current user's access.
*/
function ldap_courses_sync_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
$protected_user_fields = [
'field_user_courses',
];
if (
$field_definition->getTargetEntityTypeId() === 'user' &&
in_array($field_definition->getName(), $protected_user_fields, TRUE) &&
$operation === 'edit'
) {
// Sempre nega edição via UI/API, inclusive para o uid 1.
// Saves programáticos (ldap_user, drush) não passam por este hook.
return AccessResult::forbidden('This field is managed exclusively by LDAP synchronization.');
}
return AccessResult::neutral();
}
/**
* Implements hook_user_presave().
*
* Protects fields managed by the LDAP sync from unauthorized programmatic
* changes on existing users. Allows:
* - Saves during authorized LDAP sync (LdapCoursesSync::$syncing TRUE)
* - New user creation (initial provisioning via ldap_user)
* - Saves by users with the 'edit ldap managed user course fields' permission
*/
function ldap_courses_sync_user_presave(\Drupal\user\UserInterface $user) {
// Permite durante qualquer sync LDAP autorizado.
if (LdapCoursesSync::isSyncing()) {
return;
}
// Permite na criação inicial do usuário (primeiro login LDAP).
if ($user->isNew()) {
return;
}
// Permite se o usuário atual tem a permissão de bypass.
if (\Drupal::currentUser()->hasPermission('edit ldap managed user course fields')) {
return;
}
$original = $user->original;
if ($original === NULL) {
return;
}
// Fields managed by LDAP that cannot be changed externally.
$protected_fields = [
'field_user_courses',
];
foreach ($protected_fields as $field_name) {
if (!$user->hasField($field_name)) {
continue;
}
$original_value = $original->get($field_name)->getValue();
$new_value = $user->get($field_name)->getValue();
if ($original_value !== $new_value) {
$user->set($field_name, $original_value);
\Drupal::logger('ldap_courses_sync')->warning(
'Unauthorized attempt to change @field on user @username was blocked. Use LDAP synchronization to update this field.',
[
'@field' => $field_name,
'@username' => $user->getAccountName(),
]
);
}
}
}