From 72813e0f30af8ad44eeca7dad005dd12da8971b8 Mon Sep 17 00:00:00 2001 From: "Quintino A. G. Souza" Date: Mon, 2 Mar 2026 09:03:10 -0300 Subject: [PATCH] =?UTF-8?q?Centraliza=20configura=C3=A7=C3=A3o=20de=20acce?= =?UTF-8?q?ss=20rules=20no=20m=C3=B3dulo=20pai?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona rota, menu e abas unificados em ldap_groups_sync - Cria LdapGroupsSyncController (overview dos submódulos) - Cria UnifiedAccessRulesForm: tabela combinada de regras de todos os submódulos habilitados, com único botão "Add Rule" - Cria GlobalAccessRuleForm: estende AccessRuleFormBase com parâmetro {group_type} na rota; exibe select "Group Type" com AJAX rebuild ao criar novas regras (desabilitado ao editar) - Remove rotas access_rules e access_rule_form dos submódulos - Remove entradas de menu dos submódulos (módulo pai fornece a entrada) - Atualiza abas dos submódulos para base_route: ldap_groups_sync.config Co-Authored-By: Claude Sonnet 4.6 --- .../ldap_departments_sync.links.menu.yml | 7 +- .../ldap_departments_sync.links.task.yml | 10 +- .../ldap_departments_sync.routing.yml | 18 -- ldap_groups_sync.links.menu.yml | 6 + ldap_groups_sync.links.task.yml | 11 + ldap_groups_sync.permissions.yml | 4 + ldap_groups_sync.routing.yml | 26 ++ .../ldap_research_groups_sync.links.menu.yml | 7 +- .../ldap_research_groups_sync.links.task.yml | 12 +- .../ldap_research_groups_sync.routing.yml | 18 -- src/Controller/LdapGroupsSyncController.php | 50 ++++ src/Form/GlobalAccessRuleForm.php | 155 ++++++++++ src/Form/UnifiedAccessRulesForm.php | 283 ++++++++++++++++++ 13 files changed, 542 insertions(+), 65 deletions(-) create mode 100644 ldap_groups_sync.links.menu.yml create mode 100644 ldap_groups_sync.links.task.yml create mode 100644 ldap_groups_sync.permissions.yml create mode 100644 ldap_groups_sync.routing.yml create mode 100644 src/Controller/LdapGroupsSyncController.php create mode 100644 src/Form/GlobalAccessRuleForm.php create mode 100644 src/Form/UnifiedAccessRulesForm.php diff --git a/ldap_departments_sync/ldap_departments_sync.links.menu.yml b/ldap_departments_sync/ldap_departments_sync.links.menu.yml index 83f4e0c..456d3c8 100644 --- a/ldap_departments_sync/ldap_departments_sync.links.menu.yml +++ b/ldap_departments_sync/ldap_departments_sync.links.menu.yml @@ -1,6 +1 @@ -ldap_departments_sync.config: - title: 'LDAP Departments Sync' - description: 'Configurar sincronização de departamentos do LDAP' - route_name: ldap_departments_sync.config - parent: site_tools.admin_config - weight: 5 +# Menu entry removed: the parent module ldap_groups_sync provides the unified menu entry. diff --git a/ldap_departments_sync/ldap_departments_sync.links.task.yml b/ldap_departments_sync/ldap_departments_sync.links.task.yml index a8a4334..6759678 100644 --- a/ldap_departments_sync/ldap_departments_sync.links.task.yml +++ b/ldap_departments_sync/ldap_departments_sync.links.task.yml @@ -1,11 +1,5 @@ ldap_departments_sync.tab.config: - title: 'Configuration' + title: 'Departments Sync' route_name: ldap_departments_sync.config - base_route: ldap_departments_sync.config - weight: 0 - -ldap_departments_sync.tab.access_rules: - title: 'Access Rules' - route_name: ldap_departments_sync.access_rules - base_route: ldap_departments_sync.config + base_route: ldap_groups_sync.config weight: 10 diff --git a/ldap_departments_sync/ldap_departments_sync.routing.yml b/ldap_departments_sync/ldap_departments_sync.routing.yml index f208140..c9fd149 100644 --- a/ldap_departments_sync/ldap_departments_sync.routing.yml +++ b/ldap_departments_sync/ldap_departments_sync.routing.yml @@ -5,21 +5,3 @@ ldap_departments_sync.config: _title: 'LDAP Departments Sync' requirements: _permission: 'administer ldap departments sync' - -ldap_departments_sync.access_rules: - path: '/admin/config/local-modules/ldap-departments-sync/access-rules' - defaults: - _form: '\Drupal\ldap_departments_sync\Form\AccessRulesForm' - _title: 'Access Rules' - requirements: - _permission: 'administer ldap departments sync' - -ldap_departments_sync.access_rule_form: - path: '/admin/config/local-modules/ldap-departments-sync/access-rule/{rule_index}' - defaults: - _form: '\Drupal\ldap_departments_sync\Form\AccessRuleForm' - _title: 'Access Rule' - rule_index: 'new' - requirements: - _permission: 'administer ldap departments sync' - rule_index: 'new|\d+' diff --git a/ldap_groups_sync.links.menu.yml b/ldap_groups_sync.links.menu.yml new file mode 100644 index 0000000..9446640 --- /dev/null +++ b/ldap_groups_sync.links.menu.yml @@ -0,0 +1,6 @@ +ldap_groups_sync.config: + title: 'LDAP Groups Sync' + description: 'Configurar sincronização de grupos do LDAP' + route_name: ldap_groups_sync.config + parent: site_tools.admin_config + weight: 4 diff --git a/ldap_groups_sync.links.task.yml b/ldap_groups_sync.links.task.yml new file mode 100644 index 0000000..105762e --- /dev/null +++ b/ldap_groups_sync.links.task.yml @@ -0,0 +1,11 @@ +ldap_groups_sync.tab.overview: + title: 'Overview' + route_name: ldap_groups_sync.config + base_route: ldap_groups_sync.config + weight: 0 + +ldap_groups_sync.tab.access_rules: + title: 'Access Rules' + route_name: ldap_groups_sync.access_rules + base_route: ldap_groups_sync.config + weight: 100 diff --git a/ldap_groups_sync.permissions.yml b/ldap_groups_sync.permissions.yml new file mode 100644 index 0000000..9fc04e4 --- /dev/null +++ b/ldap_groups_sync.permissions.yml @@ -0,0 +1,4 @@ +administer ldap groups sync: + title: 'Administrar LDAP Groups Sync' + description: 'Acesso à página unificada de configuração e regras de acesso.' + restrict access: true diff --git a/ldap_groups_sync.routing.yml b/ldap_groups_sync.routing.yml new file mode 100644 index 0000000..ed5af7b --- /dev/null +++ b/ldap_groups_sync.routing.yml @@ -0,0 +1,26 @@ +ldap_groups_sync.config: + path: '/admin/config/local-modules/ldap-groups-sync' + defaults: + _controller: '\Drupal\ldap_groups_sync\Controller\LdapGroupsSyncController::overview' + _title: 'LDAP Groups Sync' + requirements: + _permission: 'administer ldap groups sync' + +ldap_groups_sync.access_rules: + path: '/admin/config/local-modules/ldap-groups-sync/access-rules' + defaults: + _form: '\Drupal\ldap_groups_sync\Form\UnifiedAccessRulesForm' + _title: 'Access Rules' + requirements: + _permission: 'administer ldap groups sync' + +ldap_groups_sync.access_rule_form: + path: '/admin/config/local-modules/ldap-groups-sync/access-rule/{group_type}/{rule_index}' + defaults: + _form: '\Drupal\ldap_groups_sync\Form\GlobalAccessRuleForm' + _title: 'Access Rule' + rule_index: 'new' + requirements: + _permission: 'administer ldap groups sync' + rule_index: 'new|\d+' + group_type: 'departments|research_groups' diff --git a/ldap_research_groups_sync/ldap_research_groups_sync.links.menu.yml b/ldap_research_groups_sync/ldap_research_groups_sync.links.menu.yml index 9687b76..456d3c8 100644 --- a/ldap_research_groups_sync/ldap_research_groups_sync.links.menu.yml +++ b/ldap_research_groups_sync/ldap_research_groups_sync.links.menu.yml @@ -1,6 +1 @@ -ldap_research_groups_sync.config: - title: 'LDAP Research Groups Sync' - description: 'Configurar sincronização de grupos de pesquisa do LDAP' - route_name: ldap_research_groups_sync.config - parent: site_tools.admin_config - weight: 6 +# Menu entry removed: the parent module ldap_groups_sync provides the unified menu entry. diff --git a/ldap_research_groups_sync/ldap_research_groups_sync.links.task.yml b/ldap_research_groups_sync/ldap_research_groups_sync.links.task.yml index 9e9dbff..5b02820 100644 --- a/ldap_research_groups_sync/ldap_research_groups_sync.links.task.yml +++ b/ldap_research_groups_sync/ldap_research_groups_sync.links.task.yml @@ -1,11 +1,5 @@ ldap_research_groups_sync.tab.config: - title: 'Configuration' + title: 'Research Groups Sync' route_name: ldap_research_groups_sync.config - base_route: ldap_research_groups_sync.config - weight: 0 - -ldap_research_groups_sync.tab.access_rules: - title: 'Access Rules' - route_name: ldap_research_groups_sync.access_rules - base_route: ldap_research_groups_sync.config - weight: 10 + base_route: ldap_groups_sync.config + weight: 20 diff --git a/ldap_research_groups_sync/ldap_research_groups_sync.routing.yml b/ldap_research_groups_sync/ldap_research_groups_sync.routing.yml index a4be2e7..582bf29 100644 --- a/ldap_research_groups_sync/ldap_research_groups_sync.routing.yml +++ b/ldap_research_groups_sync/ldap_research_groups_sync.routing.yml @@ -5,21 +5,3 @@ ldap_research_groups_sync.config: _title: 'LDAP Research Groups Sync' requirements: _permission: 'administer ldap research groups sync' - -ldap_research_groups_sync.access_rules: - path: '/admin/config/local-modules/ldap-research-groups-sync/access-rules' - defaults: - _form: '\Drupal\ldap_research_groups_sync\Form\AccessRulesForm' - _title: 'Access Rules' - requirements: - _permission: 'administer ldap research groups sync' - -ldap_research_groups_sync.access_rule_form: - path: '/admin/config/local-modules/ldap-research-groups-sync/access-rule/{rule_index}' - defaults: - _form: '\Drupal\ldap_research_groups_sync\Form\AccessRuleForm' - _title: 'Access Rule' - rule_index: 'new' - requirements: - _permission: 'administer ldap research groups sync' - rule_index: 'new|\d+' diff --git a/src/Controller/LdapGroupsSyncController.php b/src/Controller/LdapGroupsSyncController.php new file mode 100644 index 0000000..7ec47c1 --- /dev/null +++ b/src/Controller/LdapGroupsSyncController.php @@ -0,0 +1,50 @@ + [ + 'label' => $this->t('LDAP Departments Sync'), + 'description' => $this->t('Synchronizes department groups from LDAP.'), + ], + 'ldap_research_groups_sync' => [ + 'label' => $this->t('LDAP Research Groups Sync'), + 'description' => $this->t('Synchronizes research group entities from LDAP.'), + ], + ]; + + $rows = []; + foreach ($submodules as $module => $info) { + $enabled = $this->moduleHandler()->moduleExists($module); + $rows[] = [ + (string) $info['label'], + (string) $info['description'], + $enabled + ? ['data' => ['#markup' => '✓ ' . $this->t('Enabled') . '']] + : ['data' => ['#markup' => '✗ ' . $this->t('Disabled') . '']], + ]; + } + + return [ + '#type' => 'table', + '#header' => [ + $this->t('Module'), + $this->t('Description'), + $this->t('Status'), + ], + '#rows' => $rows, + ]; + } + +} diff --git a/src/Form/GlobalAccessRuleForm.php b/src/Form/GlobalAccessRuleForm.php new file mode 100644 index 0000000..52806a7 --- /dev/null +++ b/src/Form/GlobalAccessRuleForm.php @@ -0,0 +1,155 @@ +moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('entity_type.manager'), + $container->get('entity_type.bundle.info'), + $container->get('entity_field.manager'), + $container->get('current_route_match'), + $container->get('module_handler') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'ldap_groups_sync_global_access_rule_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $group_type = 'departments', $rule_index = 'new'): array { + // Priority for group type: + // 1. User's current selection submitted via AJAX (getValue). + // 2. Previously stored value across AJAX rebuilds (get). + // 3. Route parameter (initial page load). + $selected = $form_state->getValue('group_type'); + if ($selected !== NULL) { + $this->groupType = $selected; + } + else { + $this->groupType = $form_state->get('group_type') ?? $group_type; + } + $form_state->set('group_type', $this->groupType); + + // Resolve rule index the same way the parent will, so we know whether + // this is a new rule before calling parent::buildForm(). + $is_new = ($form_state->get('rule_index') ?? $rule_index) === 'new'; + + $form = parent::buildForm($form, $form_state, $rule_index); + + // Build options from enabled submodules only. + $options = []; + if ($this->moduleHandler->moduleExists('ldap_departments_sync')) { + $options['departments'] = $this->t('Departments'); + } + if ($this->moduleHandler->moduleExists('ldap_research_groups_sync')) { + $options['research_groups'] = $this->t('Research Groups'); + } + + $form['group_type'] = [ + '#type' => 'select', + '#title' => $this->t('Group Type'), + '#options' => $options, + '#default_value' => $this->groupType, + '#required' => TRUE, + '#disabled' => !$is_new, + '#weight' => -100, + ]; + + // Only attach AJAX when the field is editable (new rules). + if ($is_new) { + $form['group_type']['#ajax'] = [ + 'callback' => '::updateDependentFields', + 'wrapper' => 'access-rule-form-wrapper', + 'effect' => 'fade', + ]; + } + + return $form; + } + + /** + * {@inheritdoc} + */ + protected function getConfigName(): string { + return match($this->groupType) { + 'research_groups' => 'ldap_research_groups_sync.settings', + default => 'ldap_departments_sync.settings', + }; + } + + /** + * {@inheritdoc} + */ + protected function getDefaultGroupTypeId(): string { + return match($this->groupType) { + 'research_groups' => 'research_group', + default => 'departments', + }; + } + + /** + * {@inheritdoc} + */ + protected function getAccessRulesRoute(): string { + return 'ldap_groups_sync.access_rules'; + } + +} diff --git a/src/Form/UnifiedAccessRulesForm.php b/src/Form/UnifiedAccessRulesForm.php new file mode 100644 index 0000000..6327575 --- /dev/null +++ b/src/Form/UnifiedAccessRulesForm.php @@ -0,0 +1,283 @@ + [ + 'config' => 'ldap_departments_sync.settings', + 'module' => 'ldap_departments_sync', + 'label' => 'Departments', + ], + 'research_groups' => [ + 'config' => 'ldap_research_groups_sync.settings', + 'module' => 'ldap_research_groups_sync', + 'label' => 'Research Groups', + ], + ]; + + /** + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * Constructor. + */ + public function __construct( + ConfigFactoryInterface $config_factory, + EntityTypeManagerInterface $entity_type_manager, + ModuleHandlerInterface $module_handler + ) { + $this->configFactory = $config_factory; + $this->entityTypeManager = $entity_type_manager; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('entity_type.manager'), + $container->get('module_handler') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'ldap_groups_sync_unified_access_rules_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; + + $form['description'] = [ + '#type' => 'markup', + '#markup' => '

' . $this->t( + 'Define rules that restrict or grant entity operations based on group membership.
' + . 'Restrictive: if a matching rule exists and the user does not satisfy it, access is denied.
' + . 'Additive: the rule only grants extra access; other users keep their default permissions.' + ) . '

', + ]; + + $form['summary'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Type'), + $this->t('Label'), + $this->t('Entity type / Bundle'), + $this->t('Operations'), + $this->t('Groups / Roles'), + $this->t('Mode'), + $this->t('Status'), + $this->t('Edit'), + $this->t('Remove'), + ], + '#empty' => $this->t('No access rules defined. Use the "Add Rule" buttons below to create the first one.'), + '#attributes' => ['class' => ['access-rules-summary-table']], + '#tree' => TRUE, + ]; + + foreach (self::GROUP_TYPES as $group_type => $info) { + if (!$this->moduleHandler->moduleExists($info['module'])) { + continue; + } + + $rules = $this->configFactory->get($info['config'])->get('access_rules') ?? []; + + foreach ($rules as $i => $rule) { + $row_key = $group_type . '__' . $i; + + $bundle_part = !empty($rule['bundle']) + ? ' / ' . $rule['bundle'] . '' + : ' / ' . $this->t('all bundles') . ''; + + $form['summary'][$row_key]['type'] = [ + '#markup' => $info['label'], + ]; + + $form['summary'][$row_key]['label'] = [ + '#markup' => '' . ($rule['label'] ?: $this->t('(no label)')) . '', + ]; + + $form['summary'][$row_key]['entity_bundle'] = [ + '#markup' => ($rule['entity_type'] ?? '?') . $bundle_part, + ]; + + $form['summary'][$row_key]['operations'] = [ + '#markup' => implode(', ', $rule['operations'] ?? []), + ]; + + $membership_lines = []; + foreach ($rule['memberships'] ?? [] as $mem) { + try { + $group = $this->entityTypeManager->getStorage('group')->load($mem['group_id'] ?? 0); + $group_label = $group ? $group->label() : '#' . ($mem['group_id'] ?? '?'); + } + catch (\Exception $e) { + $group_label = '#' . ($mem['group_id'] ?? '?'); + } + $roles = implode(', ', $mem['roles'] ?? []); + $membership_lines[] = $group_label . ($roles ? ' (' . $roles . ')' : ''); + } + $form['summary'][$row_key]['memberships_summary'] = [ + '#markup' => $membership_lines + ? implode('
', $membership_lines) + : '' . $this->t('none') . '', + ]; + + $form['summary'][$row_key]['mode'] = [ + '#markup' => ($rule['mode'] ?? 'restrictive') === 'additive' + ? $this->t('Additive') + : $this->t('Restrictive'), + ]; + + $form['summary'][$row_key]['enabled'] = [ + '#markup' => ($rule['enabled'] ?? TRUE) + ? '✓ ' . $this->t('On') . '' + : '✗ ' . $this->t('Off') . '', + ]; + + $form['summary'][$row_key]['edit'] = [ + '#type' => 'link', + '#title' => $this->t('Edit'), + '#url' => Url::fromRoute('ldap_groups_sync.access_rule_form', [ + 'group_type' => $group_type, + 'rule_index' => $i, + ]), + '#attributes' => [ + 'class' => ['use-ajax', 'button', 'button--small'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode([ + 'width' => 860, + 'title' => $this->t('Edit Access Rule'), + ]), + ], + ]; + + $form['summary'][$row_key]['remove'] = [ + '#type' => 'checkbox', + '#default_value' => FALSE, + ]; + } + } + + $form['actions'] = ['#type' => 'actions']; + + // Find the first enabled submodule to use as the initial group_type in the + // modal URL. The modal itself exposes a "Group Type" select so the user can + // switch to any enabled type without closing and re-opening the dialog. + $default_group_type = NULL; + foreach (self::GROUP_TYPES as $group_type => $info) { + if ($this->moduleHandler->moduleExists($info['module'])) { + $default_group_type = $group_type; + break; + } + } + + if ($default_group_type !== NULL) { + $form['actions']['add_rule'] = [ + '#type' => 'link', + '#title' => $this->t('Add Rule'), + '#url' => Url::fromRoute('ldap_groups_sync.access_rule_form', [ + 'group_type' => $default_group_type, + 'rule_index' => 'new', + ]), + '#attributes' => [ + 'class' => ['use-ajax', 'button', 'button--primary'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode([ + 'width' => 860, + 'title' => $this->t('New Access Rule'), + ]), + ], + ]; + } + + $form['actions']['remove_selected'] = [ + '#type' => 'submit', + '#value' => $this->t('Remove Selected'), + '#limit_validation_errors' => [['summary']], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $summary_values = $form_state->getValue('summary') ?? []; + + // Collect indices to remove per group type. + $to_remove = []; + foreach ($summary_values as $row_key => $row) { + if (empty($row['remove'])) { + continue; + } + // row_key format: {group_type}__{rule_index} + [$group_type, $rule_index] = explode('__', $row_key, 2); + $to_remove[$group_type][] = (int) $rule_index; + } + + foreach (self::GROUP_TYPES as $group_type => $info) { + if (!isset($to_remove[$group_type])) { + continue; + } + if (!$this->moduleHandler->moduleExists($info['module'])) { + continue; + } + + $config = $this->configFactory->getEditable($info['config']); + $rules = $config->get('access_rules') ?? []; + + $kept = []; + foreach ($rules as $i => $rule) { + if (!in_array($i, $to_remove[$group_type], TRUE)) { + $kept[] = $rule; + } + } + + $config->set('access_rules', array_values($kept))->save(); + } + + $this->messenger()->addStatus($this->t('Access rules updated.')); + } + +}