Inicializa módulo base ldap_groups_sync

Cria super-módulo com infraestrutura compartilhada de regras de acesso
para os módulos de sincronização LDAP de grupos.

- GroupAccessRulesService: serviço parametrizável por config name
- AccessRulesFormBase: listagem/remoção de regras (classe abstrata)
- AccessRuleFormBase: formulário modal de criação/edição (classe abstrata)
- Sub-módulos ldap_departments_sync e ldap_research_groups_sync refatorados
  para estender as classes base com subclasses mínimas
- Traduções pt-br centralizadas em ldap_groups_sync.pt-br.po

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 09:55:54 -03:00
commit 346b897e25
104 changed files with 11315 additions and 0 deletions

99
README.md Normal file
View File

@@ -0,0 +1,99 @@
# LDAP Groups Sync
Módulo base que provê a infraestrutura compartilhada de **regras de acesso** para os módulos de sincronização LDAP de grupos.
Não realiza sincronização por si só — funciona como biblioteca de classes abstratas instanciada pelos sub-módulos que ficam dentro deste repositório.
## Sub-módulos incluídos
| Módulo | Tipo de grupo | Rota de configuração |
|---|---|---|
| `ldap_departments_sync` | `departments` | `/admin/config/local-modules/ldap-departments-sync` |
| `ldap_research_groups_sync` | `research_group` | `/admin/config/local-modules/ldap-research-groups-sync` |
## Estrutura
```
ldap_groups_sync/
├── src/
│ ├── GroupAccessRulesService.php # serviço de verificação de acesso
│ └── Form/
│ ├── AccessRulesFormBase.php # listagem e remoção de regras
│ └── AccessRuleFormBase.php # criação/edição de regra (modal)
├── translations/
│ └── ldap_groups_sync.pt-br.po
├── ldap_departments_sync/ # sub-módulo
└── ldap_research_groups_sync/ # sub-módulo
```
## Regras de acesso
A infraestrutura de regras de acesso permite configurar, por interface, quais operações (`create`, `update`, `delete`, `view`) cada entidade permite — e a quais membros de grupo com quais papéis.
Cada regra define:
- **Tipo de entidade / bundle** ao qual se aplica
- **Operações** controladas
- **Modo**: _Restritivo_ (nega quem não satisfaz) ou _Aditivo_ (só concede, sem negar)
- **Requisitos de membro**: um ou mais pares (grupo + papéis requeridos)
- **Condições de campo** opcionais avaliadas sobre a entidade existente (para `update`/`delete`/`view`)
## Como adicionar um novo sub-módulo
1. Crie o diretório do módulo dentro de `ldap_groups_sync/`.
2. Declare `ldap_groups_sync:ldap_groups_sync` como dependência no `.info.yml`.
3. No `.services.yml`, use `GroupAccessRulesService` com o config name como terceiro argumento:
```yaml
meu_modulo.access_rules:
class: Drupal\ldap_groups_sync\GroupAccessRulesService
arguments: ['@config.factory', '@entity_type.manager', 'meu_modulo.settings']
```
4. Crie `src/Form/AccessRulesForm.php` estendendo `AccessRulesFormBase`:
```php
namespace Drupal\meu_modulo\Form;
use Drupal\ldap_groups_sync\Form\AccessRulesFormBase;
class AccessRulesForm extends AccessRulesFormBase {
protected function getConfigName(): string { return 'meu_modulo.settings'; }
public function getFormId(): string { return 'meu_modulo_access_rules_form'; }
protected function getAccessRuleFormRoute(): string { return 'meu_modulo.access_rule_form'; }
}
```
5. Crie `src/Form/AccessRuleForm.php` estendendo `AccessRuleFormBase`:
```php
namespace Drupal\meu_modulo\Form;
use Drupal\ldap_groups_sync\Form\AccessRuleFormBase;
class AccessRuleForm extends AccessRuleFormBase {
protected function getConfigName(): string { return 'meu_modulo.settings'; }
public function getFormId(): string { return 'meu_modulo_access_rule_form'; }
protected function getAccessRulesRoute(): string { return 'meu_modulo.access_rules'; }
protected function getDefaultGroupTypeId(): string { return 'meu_group_type'; }
}
```
## Dependências
- `drupal:options`
- `drupal:telephone`
- `group:group`
- `ldap:ldap_servers`
- `site_tools`
## Licença
GPL-2.0-or-later
## Autor
Desenvolvido para o IME - UNICAMP

View File

@@ -0,0 +1,46 @@
# Changelog
All notable changes to the LDAP Departments Sync module will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2025-11-07
### Added
- Initial stable release of LDAP Departments Sync module
- LDAP server and query configuration with dynamic dropdowns
- Hierarchical group organization based on LDAP attributes
- Dynamic attribute mapping system for flexible field configuration
- User reference mapping with DN to Drupal User resolution
- Automatic search attribute detection from LDAP server configuration
- Comprehensive cron validation to prevent execution with incomplete configurations
- Administrative interface with management buttons for LDAP queries
- Extensive logging and error handling throughout the synchronization process
- Support for both manual and automatic synchronization via cron
### Features
- **LDAP Integration**: Full integration with `ldap_servers` and `ldap_query` modules
- **Dynamic Configuration**: AJAX-powered form with real-time updates based on selections
- **Hierarchical Groups**: Automatic parent-child relationship mapping from LDAP data
- **User Reference Mapping**: Convert LDAP DNs to Drupal User references automatically
- **Flexible Mapping**: Support for both simple text and user reference field mappings
- **Validation**: Comprehensive configuration validation before cron execution
- **Management Tools**: Direct links to edit LDAP queries
- **Error Recovery**: Robust error handling with detailed logging for troubleshooting
### Technical Details
- **Drupal Version**: Compatible with Drupal 11
- **Dependencies**: group, ldap_servers
- **Architecture**: Service-based with dependency injection
- **Configuration**: YAML-based with dynamic form interface
- **Security**: Defensive coding practices with proper input validation
### Configuration
- LDAP server selection with status verification
- Query selection filtered by server with real-time updates
- Dynamic attribute mapping with type-specific processing
- Hierarchical organization with parent/child attribute configuration
- User reference mapping with automatic search attribute detection
[1.0.0]: https://github.com/your-repo/ldap_departments_sync/releases/tag/v1.0.0

View File

@@ -0,0 +1,184 @@
# LDAP Departments Sync
Módulo Drupal para sincronização de departamentos do LDAP com grupos do Drupal.
## Funcionalidades
- Sincronização automática de departamentos do LDAP para grupos no Drupal
- Mapeamento flexível de atributos LDAP para campos de grupo
- Sincronização de membros de departamentos baseada em campos de usuário
- Suporte a hierarquia de departamentos
- Mapeamento dinâmico de papéis (roles) nos grupos baseado em campos
- Campos de referência a departamentos reutilizáveis para content types
## Requisitos
- Drupal 10.x ou 11.x
- Módulo LDAP (ldap_servers, ldap_query)
- Módulo Group
- Módulo Entity Reference Views Select (opcional, para widgets avançados)
- Módulo Telephone (para campo de telefone)
- Módulo Profile (opcional, para usar field_department em profiles)
## Instalação
1. Coloque o módulo no diretório `modules/custom/ldap_departments_sync`
2. Habilite o módulo: `drush en ldap_departments_sync -y`
3. Configure em `/admin/config/ldap/ldap_departments_sync`
### Instalação da Configuração Padrão
O módulo inclui configurações opcionais que podem ser instaladas via interface:
1. Acesse `/admin/config/ldap/ldap_departments_sync`
2. Na seção "Tipo de Grupo", clique em "Instalar Configuração Padrão"
3. Isso criará:
- Tipo de grupo "Departments"
- Campos de departamento (código, sigla, tipo, telefone, sala, e-mail, coordenadores)
- Roles do grupo (admin, member, etc.)
- Displays de visualização e formulário
## Configuração
### Mapeamento de Atributos LDAP
Configure os mapeamentos entre atributos LDAP e campos de grupo em:
`/admin/config/ldap/ldap_departments_sync`
Exemplo de configuração padrão:
- `imeccDepartmentCode``field_dept_code`
- `description``label`
- `cn``field_dept_acronym`
- `imeccDepartmentType``field_dept_type`
- `imeccDepartmentCoord``field_dept_coord` (referência a usuário)
- `imeccDepartmentAssocCoord``field_dept_coord_assoc` (referência a usuário)
### Sincronização de Membros
O módulo pode sincronizar automaticamente membros para os grupos baseado em:
- Matching entre `field_user_dept_code` (usuário) e `field_dept_code` (grupo)
- Atribuição automática de roles baseado em campos de usuário
### Hierarquia de Departamentos
Para habilitar hierarquia:
1. Marque "Enable Hierarchy"
2. Configure:
- **Parent Attribute**: atributo LDAP do pai (ex: `departmentNumber`)
- **Child Attribute**: atributo LDAP do filho para matching (ex: `imeccDepartmentCode`)
## Adicionando Campos de Referência a Departamentos
O módulo fornece um **handler de seleção customizado** que facilita a criação de campos de referência a departamentos em qualquer entidade (nodes, users, profiles, etc.).
### Como Adicionar um Campo de Departamento
1. Acesse a página de gerenciamento de campos da entidade desejada:
- **Content Types**: `Structure > Content types > [Tipo] > Manage fields`
- **User**: `Configuration > People > Account settings > Manage fields`
- **Profiles**: `Configuration > People > Profile types > [Tipo] > Manage fields`
2. Clique em **"Create a new field"**
3. Em **"Add a new field"**:
- Selecione **"Reference"** como categoria
- Escolha **"Other..."** (ou "Outro...")
- Preencha o **Label** (ex: "Departamento", "Department", "Setor", etc.)
- Clique em **"Continue"**
4. Configure o field storage:
- **Type of item to reference**: Selecione **"Group"** (Grupo)
- Clique em **"Save field settings"**
5. Configure as definições do campo:
- **Reference method**: Selecione **"Department selection"**
- **Group type**: Automaticamente pré-selecionado como **"Departments"**
- **Filter by department type**: (Opcional) Marque para filtrar por tipo
- **Allowed department types**: (Opcional) Selecione tipos permitidos:
- ☐ Acadêmico
- ☐ Administrativo
- Configure outras opções conforme necessário:
- **Required**: Se o campo é obrigatório
- **Help text**: Texto de ajuda
- **Default value**: Valor padrão
- Clique em **"Save settings"**
### Vantagens desta Abordagem
-**Nomes customizáveis**: Escolha qualquer machine name para o campo
-**Múltiplos campos**: Adicione vários campos de departamento no mesmo bundle com propósitos diferentes
-**Filtragem por tipo**: Opção de filtrar apenas departamentos acadêmicos ou administrativos
-**Pré-configurado**: O handler "Department selection" já vem configurado com as opções corretas
-**Flexível**: Use em qualquer tipo de entidade (nodes, users, profiles, custom entities, etc.)
### Exemplos de Uso
**Múltiplos campos no mesmo content type:**
```
Article:
- field_primary_department (obrigatório, todos os tipos)
- field_secondary_department (opcional, todos os tipos)
- field_academic_dept (opcional, apenas acadêmicos)
```
**Diferentes configurações por bundle:**
```
Student Profile:
- field_department (obrigatório, apenas acadêmicos)
Staff Profile:
- field_department (obrigatório, apenas administrativos)
```
## Sincronização Manual
Execute via Drush:
```bash
drush ldap-departments-sync
```
Ou via interface em `/admin/config/ldap/ldap_departments_sync` clicando em "Sync Now".
## Sincronização Automática (Cron)
O módulo sincroniza automaticamente durante a execução do cron do Drupal.
## Estrutura de Campos do Grupo
Campos padrão criados para o group type "departments":
- `field_dept_code` - Código do departamento (texto, obrigatório, único)
- `field_dept_acronym` - Sigla/acrônimo
- `field_dept_type` - Tipo (acadêmico/administrativo)
- `field_dept_phone` - Telefone
- `field_dept_room` - Sala
- `field_dept_mail` - E-mail
- `field_dept_coord` - Coordenador (referência a usuário)
- `field_dept_coord_assoc` - Coordenador associado (referência a usuário)
- `field_parent_group` - Grupo pai (para hierarquia)
## Campos de Usuário
Campos necessários no user entity para sincronização de membros:
- `field_user_dept_code` - Código do departamento do usuário
- `field_user_department` - Referência ao grupo de departamento
## Desenvolvimento
### Hooks Disponíveis
(Documentação futura)
### API
(Documentação futura)
## Licença
GPL-2.0-or-later
## Autor
Desenvolvido para o IME - UNICAMP

View File

@@ -0,0 +1,127 @@
langcode: pt-br
status: true
dependencies:
config:
- field.field.group.departments.field_dept_acronym
- field.field.group.departments.field_dept_code
- field.field.group.departments.field_dept_coord
- field.field.group.departments.field_dept_coord_assoc
- field.field.group.departments.field_dept_mail
- field.field.group.departments.field_dept_phone
- field.field.group.departments.field_dept_room
- field.field.group.departments.field_dept_type
- field.field.group.departments.field_parent_group
- group.type.departments
module:
- path
id: group.departments.default
targetEntityType: group
bundle: departments
mode: default
content:
field_dept_acronym:
type: string_textfield
weight: 123
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
field_dept_code:
type: string_textfield
weight: 122
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
field_dept_coord:
type: entity_reference_autocomplete
weight: 128
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
field_dept_coord_assoc:
type: entity_reference_autocomplete
weight: 129
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
field_dept_mail:
type: email_default
weight: 125
region: content
settings:
placeholder: ''
size: 60
third_party_settings: { }
field_dept_phone:
type: string_textfield
weight: 124
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
field_dept_room:
type: string_textfield
weight: 121
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
field_dept_type:
type: options_select
weight: 127
region: content
settings: { }
third_party_settings: { }
field_parent_group:
type: entity_reference_autocomplete
weight: 20
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
label:
type: string_textfield
weight: -5
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
langcode:
type: language_select
weight: 2
region: content
settings:
include_locked: true
third_party_settings: { }
path:
type: path
weight: 30
region: content
settings: { }
third_party_settings: { }
status:
type: boolean_checkbox
weight: 120
region: content
settings:
display_label: true
third_party_settings: { }
hidden:
uid: true

View File

@@ -0,0 +1,107 @@
langcode: pt-br
status: true
dependencies:
config:
- field.field.group.departments.field_dept_acronym
- field.field.group.departments.field_dept_code
- field.field.group.departments.field_dept_coord
- field.field.group.departments.field_dept_coord_assoc
- field.field.group.departments.field_dept_mail
- field.field.group.departments.field_dept_phone
- field.field.group.departments.field_dept_room
- field.field.group.departments.field_dept_type
- field.field.group.departments.field_parent_group
- group.type.departments
module:
- options
id: group.departments.default
targetEntityType: group
bundle: departments
mode: default
content:
field_dept_acronym:
type: string
label: above
settings:
link_to_entity: false
third_party_settings: { }
weight: -2
region: content
field_dept_code:
type: string
label: above
settings:
link_to_entity: false
third_party_settings: { }
weight: -3
region: content
field_dept_coord:
type: entity_reference_label
label: above
settings:
link: true
third_party_settings: { }
weight: 22
region: content
field_dept_coord_assoc:
type: entity_reference_label
label: above
settings:
link: true
third_party_settings: { }
weight: 23
region: content
field_dept_mail:
type: basic_string
label: above
settings: { }
third_party_settings: { }
weight: 0
region: content
field_dept_phone:
type: string
label: above
settings:
link_to_entity: false
third_party_settings: { }
weight: -1
region: content
field_dept_room:
type: string
label: above
settings:
link_to_entity: false
third_party_settings: { }
weight: -4
region: content
field_dept_type:
type: list_default
label: above
settings: { }
third_party_settings: { }
weight: 21
region: content
field_parent_group:
type: entity_reference_label
label: above
settings:
link: true
third_party_settings: { }
weight: 20
region: content
label:
type: string
label: hidden
settings:
link_to_entity: false
third_party_settings: { }
weight: -5
region: content
hidden:
changed: true
created: true
entity_print_view_epub: true
entity_print_view_pdf: true
entity_print_view_word_docx: true
langcode: true
uid: true

View File

@@ -0,0 +1,68 @@
langcode: en
status: true
dependencies:
config:
- field.field.user.user.field_user_category
- field.field.user.user.field_user_department
- field.field.user.user.field_user_dept_code
- field.field.user.user.field_user_work_phone
- field.field.user.user.user_picture
- image.style.thumbnail
module:
- image
- telephone
- user
_core:
default_config_hash: mZLyuWM9CQx2ZJVqFGSbzgFnHzudVbHBYmdU256A5Wk
id: user.user.default
targetEntityType: user
bundle: user
mode: default
content:
field_user_category:
type: string
label: above
settings:
link_to_entity: false
third_party_settings: { }
weight: 3
region: content
field_user_department:
type: entity_reference_label
label: above
settings:
link: true
third_party_settings: { }
weight: 10
region: content
field_user_work_phone:
type: telephone_link
label: above
settings:
title: ''
third_party_settings: { }
weight: 4
region: content
member_for:
settings: { }
third_party_settings: { }
weight: 1
region: content
user_picture:
type: image
label: hidden
settings:
image_link: content
image_style: thumbnail
image_loading:
attribute: lazy
third_party_settings: { }
weight: 0
region: content
hidden:
entity_print_view_epub: true
entity_print_view_pdf: true
entity_print_view_word_docx: true
field_user_dept_code: true
langcode: true

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_dept_acronym
- group.type.departments
id: group.departments.field_dept_acronym
field_name: field_dept_acronym
entity_type: group
bundle: departments
label: Sigla
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_dept_code
- group.type.departments
id: group.departments.field_dept_code
field_name: field_dept_code
entity_type: group
bundle: departments
label: Código
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,28 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_dept_coord
- group.type.departments
id: group.departments.field_dept_coord
field_name: field_dept_coord
entity_type: group
bundle: departments
label: Coordenador
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:user'
handler_settings:
target_bundles: null
sort:
field: _none
direction: ASC
auto_create: false
filter:
type: _none
include_anonymous: false
field_type: entity_reference

View File

@@ -0,0 +1,28 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_dept_coord_assoc
- group.type.departments
id: group.departments.field_dept_coord_assoc
field_name: field_dept_coord_assoc
entity_type: group
bundle: departments
label: 'Coordenador Associado'
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:user'
handler_settings:
target_bundles: null
sort:
field: _none
direction: ASC
auto_create: false
filter:
type: _none
include_anonymous: false
field_type: entity_reference

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_dept_mail
- group.type.departments
id: group.departments.field_dept_mail
field_name: field_dept_mail
entity_type: group
bundle: departments
label: Email
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: email

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_dept_phone
- group.type.departments
id: group.departments.field_dept_phone
field_name: field_dept_phone
entity_type: group
bundle: departments
label: Telefone
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_dept_room
- group.type.departments
id: group.departments.field_dept_room
field_name: field_dept_room
entity_type: group
bundle: departments
label: Sala
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
config:
- field.storage.group.field_dept_type
- group.type.departments
module:
- options
id: group.departments.field_dept_type
field_name: field_dept_type
entity_type: group
bundle: departments
label: 'Tipo'
description: 'Tipo do departamento (Administrativo ou Acadêmico)'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: list_string

View File

@@ -0,0 +1,27 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_parent_group
- group.type.departments
id: group.departments.field_parent_group
field_name: field_parent_group
entity_type: group
bundle: departments
label: 'Órgão superior'
description: 'Grupo pai na hierarquia de departamentos'
required: false
translatable: true
default_value: { }
default_value_callback: ''
settings:
handler: 'default:group'
handler_settings:
target_bundles:
departments: departments
sort:
field: _none
direction: ASC
auto_create: false
auto_create_bundle: ''
field_type: entity_reference

View File

@@ -0,0 +1,19 @@
langcode: en
status: true
dependencies:
config:
- field.storage.user.field_user_category
module:
- user
id: user.user.field_user_category
field_name: field_user_category
entity_type: user
bundle: user
label: Category
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,24 @@
langcode: en
status: true
dependencies:
config:
- field.storage.user.field_user_department
module:
- user
id: user.user.field_user_department
field_name: field_user_department
entity_type: user
bundle: user
label: Departamento
description: 'Departamento do usuário sincronizado do LDAP'
required: false
translatable: true
default_value: { }
default_value_callback: ''
settings:
handler: views
handler_settings:
view:
view_name: departments
display_name: entity_reference_1
field_type: entity_reference

View File

@@ -0,0 +1,19 @@
langcode: en
status: true
dependencies:
config:
- field.storage.user.field_user_dept_code
module:
- user
id: user.user.field_user_dept_code
field_name: field_user_dept_code
entity_type: user
bundle: user
label: 'Department Code'
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
config:
- field.storage.user.field_user_work_phone
module:
- telephone
- user
id: user.user.field_user_work_phone
field_name: field_user_work_phone
entity_type: user
bundle: user
label: 'Work Phone'
description: 'Work phone number, populated from LDAP.'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: telephone

View File

@@ -0,0 +1,20 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_dept_acronym
field_name: field_dept_acronym
entity_type: group
type: string
settings:
max_length: 255
case_sensitive: false
is_ascii: false
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,20 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_dept_code
field_name: field_dept_code
entity_type: group
type: string
settings:
max_length: 255
case_sensitive: false
is_ascii: false
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- user
id: group.field_dept_coord
field_name: field_dept_coord
entity_type: group
type: entity_reference
settings:
target_type: user
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- user
id: group.field_dept_coord_assoc
field_name: field_dept_coord_assoc
entity_type: group
type: entity_reference
settings:
target_type: user
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,17 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_dept_mail
field_name: field_dept_mail
entity_type: group
type: email
settings: { }
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,20 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_dept_phone
field_name: field_dept_phone
entity_type: group
type: string
settings:
max_length: 255
case_sensitive: false
is_ascii: false
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,20 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_dept_room
field_name: field_dept_room
entity_type: group
type: string
settings:
max_length: 255
case_sensitive: false
is_ascii: false
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,26 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- options
id: group.field_dept_type
field_name: field_dept_type
entity_type: group
type: list_string
settings:
allowed_values:
-
value: academico
label: Acadêmico
-
value: administrativo
label: Administrativo
allowed_values_function: ''
module: options
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_parent_group
field_name: field_parent_group
entity_type: group
type: entity_reference
settings:
target_type: group
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
module:
- user
id: user.field_user_category
field_name: field_user_category
entity_type: user
type: string
settings:
max_length: 255
case_sensitive: false
is_ascii: false
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,19 @@
langcode: en
status: true
dependencies:
module:
- group
- user
id: user.field_user_department
field_name: field_user_department
entity_type: user
type: entity_reference
settings:
target_type: group
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
module:
- user
id: user.field_user_dept_code
field_name: field_user_dept_code
entity_type: user
type: string
settings:
max_length: 255
case_sensitive: false
is_ascii: false
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,18 @@
langcode: en
status: true
dependencies:
module:
- telephone
- user
id: user.field_user_work_phone
field_name: field_user_work_phone
entity_type: user
type: telephone
settings: { }
module: telephone
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,45 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.departments
id: departments-admin
label: Admin
weight: 100
admin: true
scope: individual
global_role: null
group_type: departments
permissions:
- administer members
- delete group
- edit group
- leave group
- view group
- view unpublished group
- access group_media overview
- access group_node overview
- create group_node:article entity
- create group_node:page entity
- create group_node:webform entity
- delete any group_node:article entity
- delete any group_node:page entity
- delete any group_node:webform entity
- delete own group_node:article entity
- delete own group_node:page entity
- delete own group_node:webform entity
- update any group_node:article entity
- update any group_node:page entity
- update any group_node:webform entity
- update own group_node:article entity
- update own group_node:page entity
- update own group_node:webform entity
- view group_node:article entity
- view group_node:page entity
- view group_node:webform entity
- view own unpublished group_node:article entity
- view own unpublished group_node:page entity
- view own unpublished group_node:webform entity
- view unpublished group_node:article entity
- view unpublished group_node:page entity
- view unpublished group_node:webform entity

View File

@@ -0,0 +1,46 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.departments
- user.role.administrator
id: departments-admin_in
label: Administrador
weight: 102
admin: true
scope: insider
global_role: administrator
group_type: departments
permissions:
- administer members
- delete group
- edit group
- leave group
- view group
- view unpublished group
- access group_media overview
- access group_node overview
- create group_node:article entity
- create group_node:page entity
- create group_node:webform entity
- delete any group_node:article entity
- delete any group_node:page entity
- delete any group_node:webform entity
- delete own group_node:article entity
- delete own group_node:page entity
- delete own group_node:webform entity
- update any group_node:article entity
- update any group_node:page entity
- update any group_node:webform entity
- update own group_node:article entity
- update own group_node:page entity
- update own group_node:webform entity
- view group_node:article entity
- view group_node:page entity
- view group_node:webform entity
- view own unpublished group_node:article entity
- view own unpublished group_node:page entity
- view own unpublished group_node:webform entity
- view unpublished group_node:article entity
- view unpublished group_node:page entity
- view unpublished group_node:webform entity

View File

@@ -0,0 +1,46 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.departments
- user.role.administrator
id: departments-admin_out
label: Administrador
weight: 101
admin: true
scope: outsider
global_role: administrator
group_type: departments
permissions:
- administer members
- delete group
- edit group
- join group
- view group
- view unpublished group
- access group_media overview
- access group_node overview
- create group_node:article entity
- create group_node:page entity
- create group_node:webform entity
- delete any group_node:article entity
- delete any group_node:page entity
- delete any group_node:webform entity
- delete own group_node:article entity
- delete own group_node:page entity
- delete own group_node:webform entity
- update any group_node:article entity
- update any group_node:page entity
- update any group_node:webform entity
- update own group_node:article entity
- update own group_node:page entity
- update own group_node:webform entity
- view group_node:article entity
- view group_node:page entity
- view group_node:webform entity
- view own unpublished group_node:article entity
- view own unpublished group_node:page entity
- view own unpublished group_node:webform entity
- view unpublished group_node:article entity
- view unpublished group_node:page entity
- view unpublished group_node:webform entity

View File

@@ -0,0 +1,17 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.departments
- user.role.anonymous
id: departments-anonymous
label: Anônimo
weight: -102
admin: false
scope: outsider
global_role: anonymous
group_type: departments
permissions:
- view group
- view group_node:article entity
- view group_node:page entity

View File

@@ -0,0 +1,36 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.departments
- user.role.authenticated
id: departments-member
label: Member
weight: -100
admin: false
scope: insider
global_role: authenticated
group_type: departments
permissions:
- view group
- access group_media overview
- access group_node overview
- create group_node:article entity
- create group_node:page entity
- create group_node:webform entity
- delete own group_node:article entity
- delete own group_node:page entity
- delete own group_node:webform entity
- update any group_node:webform entity
- update own group_node:article entity
- update own group_node:page entity
- update own group_node:webform entity
- view group_node:article entity
- view group_node:page entity
- view group_node:webform entity
- view own unpublished group_node:article entity
- view own unpublished group_node:page entity
- view own unpublished group_node:webform entity
- view unpublished group_node:article entity
- view unpublished group_node:page entity
- view unpublished group_node:webform entity

View File

@@ -0,0 +1,17 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.departments
- user.role.authenticated
id: departments-outsider
label: Outsider
weight: -101
admin: false
scope: outsider
global_role: authenticated
group_type: departments
permissions:
- view group
- view group_node:article entity
- view group_node:page entity

View File

@@ -0,0 +1,10 @@
langcode: pt-br
status: true
dependencies: { }
id: departments
label: Departamento
description: ''
new_revision: true
creator_membership: true
creator_wizard: true
creator_roles: { }

View File

@@ -0,0 +1,2 @@
label: 'Telefone Profissional'
description: 'Número de telefone profissional, populado via LDAP.'

View File

@@ -0,0 +1,73 @@
/**
* Role mapping table styles
*/
/* Limita a largura da tabela e permite scroll horizontal se necessário */
.role-mappings-table {
max-width: 100%;
table-layout: fixed;
}
/* Define larguras fixas para cada coluna */
.role-mappings-table th:nth-child(1),
.role-mappings-table td:nth-child(1) {
width: 20%;
min-width: 150px;
}
.role-mappings-table th:nth-child(2),
.role-mappings-table td:nth-child(2) {
width: 15%;
min-width: 130px;
}
.role-mappings-table th:nth-child(3),
.role-mappings-table td:nth-child(3) {
width: 25%;
min-width: 180px;
}
.role-mappings-table th:nth-child(4),
.role-mappings-table td:nth-child(4) {
width: 30%;
min-width: 200px;
}
.role-mappings-table th:nth-child(5),
.role-mappings-table td:nth-child(5) {
width: 10%;
min-width: 80px;
text-align: center;
}
/* Estilo dos selects para limitar largura e truncar texto */
.role-mapping-field-select {
max-width: 100%;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Estilo para options dentro dos selects - mostra texto completo no dropdown */
.role-mapping-field-select option {
white-space: normal;
overflow: visible;
text-overflow: clip;
}
/* Estilo do textarea */
.role-mapping-values-textarea {
max-width: 100%;
width: 100%;
min-height: 50px;
}
/* Responsividade: em telas menores, permite scroll horizontal */
@media (max-width: 1200px) {
.role-mappings-table {
display: block;
overflow-x: auto;
white-space: nowrap;
}
}

View File

@@ -0,0 +1,13 @@
name: LDAP Departments Sync
type: module
description: 'Sincroniza departamentos do servidor LDAP com grupos via cron'
version: '1.1.0'
core_version_requirement: ^11
package: Custom
dependencies:
- ldap_groups_sync:ldap_groups_sync
- drupal:options
- drupal:telephone
- group:group
- ldap:ldap_servers
- site_tools

View File

@@ -0,0 +1,106 @@
<?php
/**
* @file
* Arquivo de instalação para ldap_departments_sync.
*/
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Implements hook_install().
*/
function ldap_departments_sync_install() {
\Drupal::messenger()->addWarning(t('Please ensure your group type has the required fields: field_dept_code, field_dept_acronym, field_dept_type, field_dept_phone, field_dept_room, field_dept_mail, and field_parent_group (for hierarchy).'));
}
/**
* Renames field_user_phone_number to field_user_work_phone.
*/
function ldap_departments_sync_update_10001() {
// 1. Create new field storage.
if (!FieldStorageConfig::loadByName('user', 'field_user_work_phone')) {
FieldStorageConfig::create([
'field_name' => 'field_user_work_phone',
'entity_type' => 'user',
'type' => 'telephone',
'cardinality' => 1,
'translatable' => TRUE,
])->save();
}
// 2. Create new field instance.
if (!FieldConfig::loadByName('user', 'user', 'field_user_work_phone')) {
FieldConfig::create([
'field_name' => 'field_user_work_phone',
'entity_type' => 'user',
'bundle' => 'user',
'label' => 'Work Phone',
'description' => 'Work phone number, populated from LDAP.',
'required' => FALSE,
'translatable' => FALSE,
])->save();
}
// 3. Copy data directly via SQL — efficient and side-effect-free.
$database = \Drupal::database();
$schema = $database->schema();
foreach (['user__', 'user_revision__'] as $prefix) {
$old_table = $prefix . 'field_user_phone_number';
$new_table = $prefix . 'field_user_work_phone';
if ($schema->tableExists($old_table) && $schema->tableExists($new_table)) {
$database->query("
INSERT INTO {{$new_table}}
(bundle, deleted, entity_id, revision_id, langcode, delta, field_user_work_phone_value)
SELECT bundle, deleted, entity_id, revision_id, langcode, delta, field_user_phone_number_value
FROM {{$old_table}}
WHERE deleted = 0
");
}
}
// 4. Update form display.
$form_display = EntityFormDisplay::load('user.user.default');
if ($form_display) {
$component = $form_display->getComponent('field_user_phone_number');
if ($component) {
$form_display->removeComponent('field_user_phone_number');
$form_display->setComponent('field_user_work_phone', $component);
$form_display->save();
}
}
// 5. Update view display.
$view_display = EntityViewDisplay::load('user.user.default');
if ($view_display) {
$component = $view_display->getComponent('field_user_phone_number');
if ($component) {
$view_display->removeComponent('field_user_phone_number');
$view_display->setComponent('field_user_work_phone', $component);
$view_display->save();
}
}
// 6. Delete old field instance then storage.
if ($old_config = FieldConfig::loadByName('user', 'user', 'field_user_phone_number')) {
$old_config->delete();
}
if ($old_storage = FieldStorageConfig::loadByName('user', 'field_user_phone_number')) {
$old_storage->delete();
}
return t('Renamed field_user_phone_number to field_user_work_phone.');
}
/**
* Implements hook_uninstall().
*/
function ldap_departments_sync_uninstall() {
// Remove configurações
\Drupal::configFactory()->getEditable('ldap_departments_sync.settings')->delete();
\Drupal::messenger()->addStatus(t('Módulo LDAP Departments Sync desinstalado.'));
}

View File

@@ -0,0 +1,5 @@
role_mapping_styles:
version: 1.x
css:
theme:
css/role-mapping.css: {}

View File

@@ -0,0 +1,6 @@
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

View File

@@ -0,0 +1,11 @@
ldap_departments_sync.tab.config:
title: 'Configuration'
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
weight: 10

View File

@@ -0,0 +1,291 @@
<?php
/**
* @file
* Module to synchronize departments 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_departments_sync\LdapDepartmentsSync;
/**
* Implements hook_help().
*/
function ldap_departments_sync_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.ldap_departments_sync':
return '<p>' . t('This module synchronizes departments from an LDAP server to groups through cron.') . '</p>';
}
}
/**
* Implements hook_cron().
*/
function ldap_departments_sync_cron() {
// Check if module is properly configured
$config = \Drupal::config('ldap_departments_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_departments_sync')->warning('Synchronization cancelled: LDAP server "@server_id" not found or inactive. Configure at /admin/config/local-modules/ldap-departments-sync', [
'@server_id' => $ldap_server_id,
]);
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_departments_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_departments_sync')->warning('Synchronization cancelled: LDAP query "@query_id" not found or inactive. Configure at /admin/config/local-modules/ldap-departments-sync', [
'@query_id' => $ldap_query_id,
]);
return;
}
}
else {
\Drupal::logger('ldap_departments_sync')->warning('Synchronization cancelled: ldap_query module not available.');
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_departments_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_departments_sync')->warning('Synchronization cancelled: group type "@type_id" not found. Configure at /admin/config/local-modules/ldap-departments-sync', [
'@type_id' => $group_type_id,
]);
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_departments_sync')->warning('Synchronization cancelled: error checking group type: @message', [
'@message' => $e->getMessage(),
]);
return;
}
// Get LDAP synchronization service
$ldap_sync = \Drupal::service('ldap_departments_sync.sync');
// Execute departments synchronization
try {
$ldap_sync->syncDepartments();
\Drupal::logger('ldap_departments_sync')->info('Departments synchronization executed successfully.');
}
catch (\Exception $e) {
\Drupal::logger('ldap_departments_sync')->error('Error in departments 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_departments_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_departments_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_departments_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_departments_sync.access_rules')
->checkCreateAccess($account, $entity_type_id, $entity_bundle ?? '');
}
/**
* Implements hook_entity_field_access().
*
* Nega acesso de edição aos campos gerenciados pelo LDAP sync em formulários
* e APIs (REST, JSON:API). Saves programáticos não são afetados.
*
* Campos protegidos no usuário:
* - field_user_category: categoria do usuário, populado pelo ldap_user.
* - field_user_dept_code: código do departamento, populado pelo ldap_user.
* - field_user_department: referência ao grupo, populado pelo nosso sync.
* Proteger este campo evita o erro de validação "Esta entidade não pode
* ser referenciada" que ocorre quando o widget tenta validar o grupo
* referenciado contra o acesso do usuário atual.
* - field_user_work_phone: telefone de trabalho, populado pelo ldap_user.
*/
function ldap_departments_sync_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
$protected_user_fields = [
'field_user_category',
'field_user_dept_code',
'field_user_department',
'field_user_work_phone',
];
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().
*
* Protege os campos gerenciados pelo LDAP sync de alterações programáticas
* não autorizadas em usuários existentes. Permite:
* - Saves durante sync LDAP autorizado (LdapDepartmentsSync::$syncing TRUE)
* - Criação de novos usuários (provisionamento inicial via ldap_user)
* - Salvas por usuários com a permissão 'edit ldap managed user fields'
*/
function ldap_departments_sync_user_presave(\Drupal\user\UserInterface $user) {
// Permite durante qualquer sync LDAP autorizado.
if (LdapDepartmentsSync::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 fields')) {
return;
}
$original = $user->original;
if ($original === NULL) {
return;
}
// Campos gerenciados pelo LDAP que não podem ser alterados externamente.
$protected_fields = [
'field_user_category',
'field_user_dept_code',
'field_user_department',
'field_user_work_phone',
];
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_departments_sync')->warning(
'Tentativa não autorizada de alterar @field do usuário @username foi bloqueada. Use a sincronização LDAP para atualizar este campo.',
[
'@field' => $field_name,
'@username' => $user->getAccountName(),
]
);
}
}
}
/**
* Implements hook_ENTITY_TYPE_predelete() for user entities.
*
* Cleans up user references in department groups when a user is deleted.
*/
function ldap_departments_sync_user_predelete(\Drupal\user\UserInterface $user) {
$config = \Drupal::config('ldap_departments_sync.settings');
$group_type_id = $config->get('group_type_id') ?: 'departments';
// Fields that may contain user references
$user_reference_fields = [
'field_dept_coord',
'field_dept_coord_assoc',
];
try {
$group_storage = \Drupal::entityTypeManager()->getStorage('group');
$user_id = $user->id();
// Find groups that reference this user
foreach ($user_reference_fields as $field_name) {
$groups = $group_storage->loadByProperties([
'type' => $group_type_id,
$field_name => $user_id,
]);
foreach ($groups as $group) {
if ($group->hasField($field_name)) {
$group->set($field_name, NULL);
$group->save();
\Drupal::logger('ldap_departments_sync')->info('Removed user @username reference from @field in group @group', [
'@username' => $user->getAccountName(),
'@field' => $field_name,
'@group' => $group->label(),
]);
}
}
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_departments_sync')->error('Error cleaning up user references: @message', [
'@message' => $e->getMessage(),
]);
}
}

View File

@@ -0,0 +1,9 @@
administer ldap departments sync:
title: 'Administrar LDAP Departments Sync'
description: 'Configurar sincronização de departamentos do LDAP'
restrict access: true
edit ldap managed user fields:
title: 'Editar campos gerenciados pelo LDAP no usuário'
description: 'Permite editar manualmente campos como field_user_dept_code que são normalmente gerenciados pelo LDAP. Use apenas em emergências.'
restrict access: true

View File

@@ -0,0 +1,25 @@
ldap_departments_sync.config:
path: '/admin/config/local-modules/ldap-departments-sync'
defaults:
_form: '\Drupal\ldap_departments_sync\Form\LdapDepartmentsSyncConfigForm'
_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+'

View File

@@ -0,0 +1,8 @@
services:
ldap_departments_sync.sync:
class: Drupal\ldap_departments_sync\LdapDepartmentsSync
arguments: ['@entity_type.manager', '@config.factory', '@logger.factory', '@ldap.bridge']
ldap_departments_sync.access_rules:
class: Drupal\ldap_groups_sync\GroupAccessRulesService
arguments: ['@config.factory', '@entity_type.manager', 'ldap_departments_sync.settings']

View File

@@ -0,0 +1,22 @@
<?php
namespace Drupal\ldap_departments_sync\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Controller para títulos traduzíveis.
*/
class LocalModulesController extends ControllerBase {
/**
* Retorna título traduzível para a página Local Modules.
*
* @return string
* Título traduzido.
*/
public function getTitle() {
return $this->t('Local Modules');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Drupal\ldap_departments_sync\Form;
use Drupal\ldap_groups_sync\Form\AccessRuleFormBase;
/**
* Modal form for creating or editing a single access rule (Departments Sync).
*/
class AccessRuleForm extends AccessRuleFormBase {
/**
* {@inheritdoc}
*/
protected function getConfigName(): string {
return 'ldap_departments_sync.settings';
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'ldap_departments_sync_access_rule_form';
}
/**
* {@inheritdoc}
*/
protected function getAccessRulesRoute(): string {
return 'ldap_departments_sync.access_rules';
}
/**
* {@inheritdoc}
*/
protected function getDefaultGroupTypeId(): string {
return 'departments';
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Drupal\ldap_departments_sync\Form;
use Drupal\ldap_groups_sync\Form\AccessRulesFormBase;
/**
* Lists and manages access rules for the LDAP Departments Sync module.
*/
class AccessRulesForm extends AccessRulesFormBase {
/**
* {@inheritdoc}
*/
protected function getConfigName(): string {
return 'ldap_departments_sync.settings';
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'ldap_departments_sync_access_rules_form';
}
/**
* {@inheritdoc}
*/
protected function getAccessRuleFormRoute(): string {
return 'ldap_departments_sync.access_rule_form';
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,105 @@
<?php
namespace Drupal\ldap_departments_sync\Plugin\EntityReferenceSelection;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'department' entity_reference selection.
*
* @EntityReferenceSelection(
* id = "ldap_departments_sync",
* label = @Translation("Department selection"),
* entity_types = {"group"},
* group = "ldap_departments_sync",
* weight = 1
* )
*/
class DepartmentSelection extends DefaultSelection {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'filter_by_type' => FALSE,
'allowed_types' => [],
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
// Força departments como bundle padrão
$form['target_bundles']['#default_value'] = ['departments' => 'departments'];
$configuration = $this->getConfiguration();
$form['filter_by_type'] = [
'#type' => 'checkbox',
'#title' => $this->t('Filter by department type'),
'#description' => $this->t('Only show departments of specific types (field_dept_type).'),
'#default_value' => $configuration['filter_by_type'],
'#weight' => 10,
];
$form['allowed_types'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Allowed department types'),
'#options' => [
'academico' => $this->t('Acadêmico'),
'administrativo' => $this->t('Administrativo'),
],
'#default_value' => $configuration['allowed_types'],
'#weight' => 11,
'#states' => [
'visible' => [
':input[name="settings[handler_settings][filter_by_type]"]' => ['checked' => TRUE],
],
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::validateConfigurationForm($form, $form_state);
// Remove valores vazios de allowed_types
$allowed_types = $form_state->getValue(['settings', 'handler_settings', 'allowed_types']);
if (is_array($allowed_types)) {
$allowed_types = array_filter($allowed_types);
$form_state->setValue(['settings', 'handler_settings', 'allowed_types'], $allowed_types);
}
}
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
$configuration = $this->getConfiguration();
// Força apenas departments
$query->condition('type', 'departments');
// Filtra por tipo de departamento se configurado
if (!empty($configuration['filter_by_type']) && !empty($configuration['allowed_types'])) {
$allowed_types = array_filter($configuration['allowed_types']);
if (!empty($allowed_types)) {
$query->condition('field_dept_type', array_keys($allowed_types), 'IN');
}
}
return $query;
}
}

View File

@@ -0,0 +1,286 @@
# Portuguese (Brazil) translation for LDAP Departments Sync module
# Copyright (c) 2025
# This file is distributed under the same license as the LDAP Departments Sync module.
#
msgid ""
msgstr ""
"Project-Id-Version: LDAP Departments Sync 1.0.0\n"
"POT-Creation-Date: 2025-11-07 12:00+0000\n"
"PO-Revision-Date: 2025-11-07 12:00+0000\n"
"Language-Team: Portuguese (Brazil)\n"
"Language: pt-br\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: src/Controller/LocalModulesController.php
msgid "Local Modules"
msgstr "Módulos Locais"
#: ldap_departments_sync.routing.yml
msgid "LDAP Departments Sync"
msgstr "Sincronização de Departamentos LDAP"
#: ldap_departments_sync.module
msgid "This module synchronizes departments from an LDAP server to groups through cron."
msgstr "Este módulo sincroniza departamentos de um servidor LDAP para grupos através do cron."
#: src/Form/LdapDepartmentsSyncConfigForm.php
msgid "Manage Fields"
msgstr "Gerenciar Campos"
msgid "LDAP Server"
msgstr "Servidor LDAP"
msgid "Select the LDAP server configured for departments synchronization."
msgstr "Selecione o servidor LDAP configurado para sincronização de departamentos."
msgid "- Select a server -"
msgstr "- Selecione um servidor -"
msgid "No LDAP server found. <a href=\"@url\">Configure an LDAP server</a> first."
msgstr "Nenhum servidor LDAP encontrado. <a href=\"@url\">Configure um servidor LDAP</a> primeiro."
msgid "LDAP Query"
msgstr "Consulta LDAP"
msgid "Select the LDAP query that will be used to search for departments."
msgstr "Selecione a consulta LDAP que será usada para buscar departamentos."
msgid "- Select a query -"
msgstr "- Selecione uma query -"
msgid "No LDAP query found for the selected server. <a href=\"@url\">Create a new LDAP query</a>."
msgstr "Nenhuma query LDAP encontrada para o servidor selecionado. <a href=\"@url\">Crie uma nova query LDAP</a>."
msgid "Create New LDAP Query"
msgstr "Criar Nova Query LDAP"
msgid "Edit Selected Query"
msgstr "Editar Query Selecionada"
msgid "Hierarchy"
msgstr "Hierarquia"
msgid "Configure whether departments should be organized hierarchically based on LDAP attributes."
msgstr "Configure se os departamentos devem ser organizados hierarquicamente baseado em atributos LDAP."
msgid "Enable hierarchy"
msgstr "Habilitar hierarquização"
msgid "When enabled, departments will be organized hierarchically based on the specified parent and child attributes."
msgstr "Quando habilitado, os departamentos serão organizados hierarquicamente baseado nos atributos parent e child especificados."
msgid "Parent Attribute"
msgstr "Atributo Pai"
msgid "LDAP attribute that contains the parent department code (e.g. departmentNumber)."
msgstr "Atributo LDAP que contém o código do departamento pai (ex: departmentNumber)."
msgid "- Select an attribute -"
msgstr "- Selecione um atributo -"
msgid "Child Attribute"
msgstr "Atributo Filho"
msgid "LDAP attribute that contains the department's own code (e.g. imeccDepartmentCode)."
msgstr "Atributo LDAP que contém o código próprio do departamento (ex: imeccDepartmentCode)."
msgid "All elements with the same child value will be nested under the corresponding parent recursively."
msgstr "Todos os elementos com o mesmo valor de child serão aninhados sob o parent correspondente recursivamente."
msgid "Attribute Mapping"
msgstr "Mapeamento de Atributos"
msgid "Configure how LDAP attributes are mapped to group fields.<br><strong>Note:</strong> For \"User Reference\" mappings, the search attribute will be automatically obtained from the \"Account name attribute\" field (or \"Authentication name attribute\" as fallback) configured in the selected LDAP server."
msgstr "Configure como os atributos LDAP são mapeados para os campos do grupo.<br><strong>Nota:</strong> Para mapeamentos de \"Referência de Usuário\", o atributo de busca será automaticamente obtido do campo \"Account name attribute\" (ou \"Authentication name attribute\" como fallback) configurado no servidor LDAP selecionado."
msgid "Entity Field"
msgstr "Campo da Entidade"
msgid "Group Field"
msgstr "Campo do Grupo"
msgid "LDAP Attribute"
msgstr "Atributo LDAP"
msgid "Mapping Type"
msgstr "Tipo de Mapeamento"
msgid "Remove"
msgstr "Remover"
msgid "No mappings configured."
msgstr "Nenhum mapeamento configurado."
msgid "- Select a field -"
msgstr "- Selecione um campo -"
msgid "Simple (text)"
msgstr "Simples (texto)"
msgid "User Reference (DN → User)"
msgstr "Referência de Usuário (DN → User)"
msgid "Add Mapping"
msgstr "Adicionar Mapeamento"
msgid "Remove Selected"
msgstr "Remover Selecionados"
msgid "Save Configuration"
msgstr "Salvar Configurações"
msgid "Test Connection"
msgstr "Testar Conexão"
msgid "Synchronize Departments"
msgstr "Sincronizar Departamentos"
msgid "Error loading LDAP servers: @message"
msgstr "Erro ao carregar servidores LDAP: @message"
msgid "Error loading group fields: @message"
msgstr "Erro ao carregar campos do grupo: @message"
msgid "Error loading query attributes: @message"
msgstr "Erro ao carregar atributos da query: @message"
msgid "LDAP attribute is required when field is selected."
msgstr "Atributo LDAP é obrigatório quando campo está selecionado."
msgid "Group field is required when attribute is selected."
msgstr "Campo do grupo é obrigatório quando atributo está selecionado."
msgid "Configuration saved successfully."
msgstr "Configurações salvas com sucesso."
msgid "Group Role Mapping"
msgstr "Mapeamento de Papéis em Grupos"
msgid "Configure how user attributes map to group roles. Each role can have its own mapping criteria."
msgstr "Configure como atributos de usuário mapeiam para papéis em grupos. Cada papel pode ter seus próprios critérios de mapeamento."
msgid "Configure how to assign group roles based on LDAP attributes or user fields."
msgstr "Configure como atribuir papéis em grupos baseado em atributos LDAP ou campos de usuário."
msgid "Enable role mapping"
msgstr "Habilitar mapeamento de papéis"
msgid "When enabled, users will be assigned group roles based on the mappings below."
msgstr "Quando habilitado, os usuários receberão papéis em grupos baseado nos mapeamentos abaixo."
msgid "Group Role"
msgstr "Papel do Grupo"
msgid "Source"
msgstr "Origem"
msgid "LDAP Attribute"
msgstr "Atributo LDAP"
msgid "User Field"
msgstr "Campo do Usuário"
msgid "Values (comma-separated)"
msgstr "Valores (separados por vírgula)"
msgid "No role mappings configured."
msgstr "Nenhum mapeamento de papel configurado."
msgid "- Select a role -"
msgstr "- Selecione um papel -"
msgid "Add Role Mapping"
msgstr "Adicionar Mapeamento de Papel"
msgid "Remove Selected"
msgstr "Remover Selecionados"
msgid "Default Group Role"
msgstr "Papel Padrão do Grupo"
msgid "The default role to assign if no role mapping matches."
msgstr "O papel padrão a ser atribuído se nenhum mapeamento corresponder."
msgid "Enter the expected values for this field/attribute, separated by commas (e.g., admin,administrator,manager)."
msgstr "Digite os valores esperados para este campo/atributo, separados por vírgula (ex: admin,administrador,gerente)."
msgid "Internal"
msgstr "Interno"
msgid "Outsider"
msgstr "Externo"
msgid "- Select attribute/field -"
msgstr "- Selecione atributo/campo -"
msgid "Group Field Match"
msgstr "Correspondência de Campo do Grupo"
msgid "User Field / LDAP Attribute"
msgstr "Campo do Usuário / Atributo LDAP"
msgid "Group Field / Values"
msgstr "Campo do Grupo / Valores"
msgid "- Select group field -"
msgstr "- Selecione campo do grupo -"
msgid "- Select user field -"
msgstr "- Selecione campo do usuário -"
msgid "The default \"departments\" group type is not installed."
msgstr "O tipo de grupo padrão \"departments\" não está instalado."
msgid "Install Default Configuration"
msgstr "Instalar Configuração Padrão"
msgid "The button above will create:<ul><li>Group type \"departments\" with all required fields</li><li>User fields for department synchronization</li><li>Default group roles and displays</li></ul>Or you can <a href=\"@url\">create a custom group type</a> manually."
msgstr "O botão acima irá criar:<ul><li>Tipo de grupo \"departments\" com todos os campos necessários</li><li>Campos de usuário para sincronização de departamento</li><li>Papéis e visualizações de grupo padrão</li></ul>Ou você pode <a href=\"@url\">criar um tipo de grupo personalizado</a> manualmente."
msgid "Successfully installed @count configuration items. Group type \"departments\" and all required fields have been created. Please reload the page to see the changes."
msgstr "@count itens de configuração instalados com sucesso. O tipo de grupo \"departments\" e todos os campos necessários foram criados. Por favor, recarregue a página para ver as alterações."
msgid "All configurations already exist. Nothing was imported."
msgstr "Todas as configurações já existem. Nada foi importado."
msgid "Error installing default configuration: @message"
msgstr "Erro ao instalar configuração padrão: @message"
msgid "Configuration directory not found: @path"
msgstr "Diretório de configuração não encontrado: @path"
msgid "Configuration @name already exists, skipping."
msgstr "Configuração @name já existe, ignorando."
#: src/Plugin/EntityReferenceSelection/DepartmentSelection.php
msgid "Filter by department type"
msgstr "Filtrar por tipo de departamento"
msgid "Only show departments of specific types (field_dept_type)."
msgstr "Mostrar apenas departamentos de tipos específicos (field_dept_type)."
msgid "Allowed department types"
msgstr "Tipos de departamento permitidos"
msgid "Acadêmico"
msgstr "Acadêmico"
msgid "Administrativo"
msgstr "Administrativo"
msgid "Department selection"
msgstr "Seleção de departamento"
#: ldap_departments_sync.links.task.yml
msgid "Configuration"
msgstr "Configuração"
msgid "Access Rules"
msgstr "Regras de Acesso"
msgid "Access Rule"
msgstr "Regra de Acesso"

View File

@@ -0,0 +1,152 @@
# Portuguese translation of LDAP Departments Sync
# Copyright (c) 2025
msgid ""
msgstr ""
"Project-Id-Version: LDAP Departments Sync 1.0.0\n"
"POT-Creation-Date: 2025-01-12 10:00-0300\n"
"PO-Revision-Date: 2025-01-12 10:00-0300\n"
"Language-Team: Portuguese (Brazil)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"Language: pt_BR\n"
# Form titles and sections
msgid "Synchronization Type"
msgstr "Tipo de Sincronização"
msgid "Data storage type"
msgstr "Tipo de armazenamento de dados"
msgid "Choose whether departments should be synchronized as taxonomy terms or as groups."
msgstr "Escolha se os departamentos devem ser sincronizados como termos de taxonomia ou como grupos."
msgid "Taxonomy Vocabulary - Store departments as taxonomy terms"
msgstr "Vocabulário de Taxonomia - Armazenar departamentos como termos de taxonomia"
msgid "Groups - Store departments as group entities"
msgstr "Grupos - Armazenar departamentos como entidades de grupo"
msgid "Updating form..."
msgstr "Atualizando formulário..."
msgid "Taxonomy Vocabulary"
msgstr "Vocabulário de Taxonomia"
msgid "Target Vocabulary"
msgstr "Vocabulário de Destino"
msgid "Select the taxonomy vocabulary where departments will be synchronized. <br><strong>Required fields:</strong> field_dept_code, field_dept_acronym, field_dept_type, field_dept_phone, field_dept_room, field_dept_mail"
msgstr "Selecione o vocabulário de taxonomia onde os departamentos serão sincronizados. <br><strong>Campos obrigatórios:</strong> field_dept_code, field_dept_acronym, field_dept_type, field_dept_phone, field_dept_room, field_dept_mail"
msgid "- Select a vocabulary -"
msgstr "- Selecione um vocabulário -"
msgid "No taxonomy vocabulary found. <a href=\"@url\">Create a vocabulary</a> first."
msgstr "Nenhum vocabulário de taxonomia encontrado. <a href=\"@url\">Crie um vocabulário</a> primeiro."
msgid "Create New Vocabulary"
msgstr "Criar Novo Vocabulário"
msgid "Manage Vocabulary"
msgstr "Gerenciar Vocabulário"
msgid "Manage Fields"
msgstr "Gerenciar Campos"
msgid "Group Type"
msgstr "Tipo de Grupo"
msgid "Target Group Type"
msgstr "Tipo de Grupo de Destino"
msgid "Select the group type where departments will be synchronized. <br><strong>Required fields:</strong> field_dept_code, field_dept_acronym, field_dept_type, field_dept_phone, field_dept_room, field_dept_mail"
msgstr "Selecione o tipo de grupo onde os departamentos serão sincronizados. <br><strong>Campos obrigatórios:</strong> field_dept_code, field_dept_acronym, field_dept_type, field_dept_phone, field_dept_room, field_dept_mail"
msgid "- Select a group type -"
msgstr "- Selecione um tipo de grupo -"
msgid "No group type found. <a href=\"@url\">Create a group type</a> first."
msgstr "Nenhum tipo de grupo encontrado. <a href=\"@url\">Crie um tipo de grupo</a> primeiro."
msgid "Create New Group Type"
msgstr "Criar Novo Tipo de Grupo"
msgid "Manage Group Type"
msgstr "Gerenciar Tipo de Grupo"
msgid "LDAP Server"
msgstr "Servidor LDAP"
msgid "Select the LDAP server configured for departments synchronization."
msgstr "Selecione o servidor LDAP configurado para sincronização de departamentos."
msgid "- Select a server -"
msgstr "- Selecione um servidor -"
msgid "No LDAP server found. <a href=\"@url\">Configure an LDAP server</a> first."
msgstr "Nenhum servidor LDAP encontrado. <a href=\"@url\">Configure um servidor LDAP</a> primeiro."
msgid "LDAP Query"
msgstr "Consulta LDAP"
msgid "Select the LDAP query that will be used to search for departments."
msgstr "Selecione a consulta LDAP que será usada para buscar departamentos."
msgid "- Select a query -"
msgstr "- Selecione uma consulta -"
msgid "No LDAP query found for the selected server. <a href=\"@url\">Create a new LDAP query</a>."
msgstr "Nenhuma consulta LDAP encontrada para o servidor selecionado. <a href=\"@url\">Crie uma nova consulta LDAP</a>."
msgid "Create New LDAP Query"
msgstr "Criar Nova Consulta LDAP"
msgid "Edit Selected Query"
msgstr "Editar Consulta Selecionada"
msgid "Hierarchy"
msgstr "Hierarquia"
msgid "Configure whether departments should be organized hierarchically based on LDAP attributes."
msgstr "Configure se os departamentos devem ser organizados hierarquicamente com base em atributos LDAP."
msgid "Enable hierarchy"
msgstr "Habilitar hierarquia"
msgid "When enabled, terms will be organized hierarchically based on the specified parent and child attributes."
msgstr "Quando habilitado, os departamentos serão organizados hierarquicamente com base nos atributos de pai e filho especificados."
msgid "Parent Attribute"
msgstr "Atributo Pai"
msgid "LDAP attribute that contains the parent department code (e.g. departmentNumber)."
msgstr "Atributo LDAP que contém o código do departamento pai (ex: departmentNumber)."
msgid "Child Attribute"
msgstr "Atributo Filho"
msgid "LDAP attribute that contains the department's own code (e.g. imeccDepartmentCode)."
msgstr "Atributo LDAP que contém o código próprio do departamento (ex: imeccDepartmentCode)."
msgid "- Select an attribute -"
msgstr "- Selecione um atributo -"
msgid "All elements with the same child value will be nested under the corresponding parent recursively."
msgstr "Todos os elementos com o mesmo valor filho serão aninhados sob o pai correspondente recursivamente."
msgid "Attribute Mapping"
msgstr "Mapeamento de Atributos"
msgid "Configure how LDAP attributes are mapped to target entity fields.<br><strong>Note:</strong> For \"User Reference\" mappings, the search attribute will be automatically obtained from the \"Account name attribute\" field (or \"Authentication name attribute\" as fallback) configured in the selected LDAP server."
msgstr "Configure como os atributos LDAP são mapeados para os campos da entidade de destino.<br><strong>Nota:</strong> Para mapeamentos de \"Referência de Usuário\", o atributo de busca será obtido automaticamente do campo \"Account name attribute\" (ou \"Authentication name attribute\" como alternativa) configurado no servidor LDAP selecionado."
msgid "- Select a field -"
msgstr "- Selecione um campo -"
msgid "Simple (text)"
msgstr "Simples (texto)"
msgid "User Reference (DN → User)"
msgstr "Referência de Usuário (DN → Usuário)"

11
ldap_groups_sync.info.yml Normal file
View File

@@ -0,0 +1,11 @@
name: LDAP Groups Sync
type: module
description: 'Base module with shared access-rules infrastructure for LDAP group sync modules.'
core_version_requirement: ^11
package: Custom
dependencies:
- drupal:options
- drupal:telephone
- group:group
- ldap:ldap_servers
- site_tools

View File

@@ -0,0 +1,69 @@
# LDAP Research Groups Sync
Módulo Drupal 11 que sincroniza grupos de pesquisa do servidor LDAP corporativo com entidades do tipo `research_group` (módulo Group).
## Funcionalidades
- Criação e atualização automática de grupos de pesquisa via cron a partir de uma LDAP query configurável
- Sincronização de membros dos grupos a partir do atributo `member` (ou outro configurável) de cada entrada LDAP
- Regras de acesso configuráveis por grupo
- Proteção dos campos LDAP do usuário contra edição não autorizada
## Dependências
- `drupal:options`
- `drupal:telephone`
- `group:group`
- `ldap:ldap_servers`
- `site_tools`
## Campos criados
### No grupo (`research_group`)
| Campo | Tipo | Descrição |
|---|---|---|
| `field_rg_code` | String | Código do grupo (chave de sincronização) |
| `field_rg_phone` | Telephone | Telefone |
| `field_rg_mail` | Email | E-mail |
| `field_rg_coord` | Entity reference (user) | Coordenador do grupo |
| `field_rg_coord_assoc` | Entity reference (user) | Coordenador associado do grupo |
| `field_rg_department` | Entity reference (group) | Departamento ao qual o grupo está vinculado |
### No usuário
| Campo | Tipo | Descrição |
|---|---|---|
| `field_user_research_groups` | Entity reference (group, múltiplos) | Grupos de pesquisa do usuário (populado pelo sync) |
## Instalação
```bash
drush en ldap_research_groups_sync
drush cr
```
## Configuração
Acesse `/admin/config/local-modules/ldap-research-groups-sync` e configure:
1. **Group Type** — selecione `research_group`
2. **LDAP Server** — servidor LDAP a ser usado
3. **LDAP Query** — query entity que retorna as entradas dos grupos de pesquisa
4. **Attribute Mappings** — mapeamento entre atributos LDAP e campos do grupo
5. **Member Synchronization** — habilite e defina o atributo LDAP de membros (padrão: `member`)
> **Atenção:** o atributo de membros (ex.: `member`) deve estar incluído nos atributos retornados pela LDAP query, ou a query não deve ter filtro de atributos.
## Sincronização manual
```bash
drush php-eval "\Drupal::service('ldap_research_groups_sync.sync')->syncResearchGroups();"
```
A sincronização também é executada automaticamente pelo cron.
## Permissões
- `administer ldap research groups sync` — acesso à página de configuração
- `edit ldap managed user rg fields` — permite editar o campo `field_user_research_groups` manualmente (normalmente reservado ao sync)

View File

@@ -0,0 +1,102 @@
langcode: pt-br
status: true
dependencies:
config:
- field.field.group.research_group.field_rg_code
- field.field.group.research_group.field_rg_coord
- field.field.group.research_group.field_rg_coord_assoc
- field.field.group.research_group.field_rg_mail
- field.field.group.research_group.field_rg_phone
- field.field.group.research_group.field_rg_department
- group.type.research_group
module:
- path
id: group.research_group.default
targetEntityType: group
bundle: research_group
mode: default
content:
field_rg_code:
type: string_textfield
weight: 122
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
field_rg_mail:
type: email_default
weight: 125
region: content
settings:
placeholder: ''
size: 60
third_party_settings: { }
field_rg_phone:
type: string_textfield
weight: 124
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
field_rg_coord:
type: entity_reference_autocomplete
weight: 126
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
field_rg_coord_assoc:
type: entity_reference_autocomplete
weight: 127
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
field_rg_department:
type: entity_reference_autocomplete
weight: 25
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
label:
type: string_textfield
weight: -5
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
langcode:
type: language_select
weight: 2
region: content
settings:
include_locked: true
third_party_settings: { }
path:
type: path
weight: 30
region: content
settings: { }
third_party_settings: { }
status:
type: boolean_checkbox
weight: 120
region: content
settings:
display_label: true
third_party_settings: { }
hidden:
uid: true

View File

@@ -0,0 +1,81 @@
langcode: pt-br
status: true
dependencies:
config:
- field.field.group.research_group.field_rg_code
- field.field.group.research_group.field_rg_coord
- field.field.group.research_group.field_rg_coord_assoc
- field.field.group.research_group.field_rg_mail
- field.field.group.research_group.field_rg_phone
- field.field.group.research_group.field_rg_department
- group.type.research_group
module:
- options
id: group.research_group.default
targetEntityType: group
bundle: research_group
mode: default
content:
field_rg_code:
type: string
label: above
settings:
link_to_entity: false
third_party_settings: { }
weight: -3
region: content
field_rg_mail:
type: basic_string
label: above
settings: { }
third_party_settings: { }
weight: 0
region: content
field_rg_phone:
type: string
label: above
settings:
link_to_entity: false
third_party_settings: { }
weight: -1
region: content
field_rg_coord:
type: entity_reference_label
label: above
settings:
link: true
third_party_settings: { }
weight: 1
region: content
field_rg_coord_assoc:
type: entity_reference_label
label: above
settings:
link: true
third_party_settings: { }
weight: 2
region: content
field_rg_department:
type: entity_reference_label
label: above
settings:
link: true
third_party_settings: { }
weight: 25
region: content
label:
type: string
label: hidden
settings:
link_to_entity: false
third_party_settings: { }
weight: -5
region: content
hidden:
changed: true
created: true
entity_print_view_epub: true
entity_print_view_pdf: true
entity_print_view_word_docx: true
langcode: true
uid: true

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_rg_code
- group.type.research_group
id: group.research_group.field_rg_code
field_name: field_rg_code
entity_type: group
bundle: research_group
label: 'Código'
description: 'Código do grupo de pesquisa (chave de sincronização LDAP)'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,30 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_rg_coord
- group.type.research_group
module:
- user
id: group.research_group.field_rg_coord
field_name: field_rg_coord
entity_type: group
bundle: research_group
label: Coordenador
description: 'Usuário responsável pela coordenação do grupo de pesquisa'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:user'
handler_settings:
target_bundles: null
sort:
field: _none
direction: ASC
auto_create: false
filter:
type: _none
include_anonymous: false
field_type: entity_reference

View File

@@ -0,0 +1,30 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_rg_coord_assoc
- group.type.research_group
module:
- user
id: group.research_group.field_rg_coord_assoc
field_name: field_rg_coord_assoc
entity_type: group
bundle: research_group
label: 'Coordenador Associado'
description: 'Usuário responsável pela coordenação associada do grupo de pesquisa'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:user'
handler_settings:
target_bundles: null
sort:
field: _none
direction: ASC
auto_create: false
filter:
type: _none
include_anonymous: false
field_type: entity_reference

View File

@@ -0,0 +1,28 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_rg_department
- group.type.department
- group.type.research_group
id: group.research_group.field_rg_department
field_name: field_rg_department
entity_type: group
bundle: research_group
label: Departamento
description: 'Departamento ao qual este grupo de pesquisa está vinculado'
required: false
translatable: true
default_value: { }
default_value_callback: ''
settings:
handler: 'default:group'
handler_settings:
target_bundles:
department: department
sort:
field: _none
direction: ASC
auto_create: false
auto_create_bundle: ''
field_type: entity_reference

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_rg_mail
- group.type.research_group
id: group.research_group.field_rg_mail
field_name: field_rg_mail
entity_type: group
bundle: research_group
label: 'E-mail'
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: email

View File

@@ -0,0 +1,20 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_rg_phone
- group.type.research_group
module:
- telephone
id: group.research_group.field_rg_phone
field_name: field_rg_phone
entity_type: group
bundle: research_group
label: 'Telefone'
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: telephone

View File

@@ -0,0 +1,28 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.user.field_user_research_groups
module:
- user
id: user.user.field_user_research_groups
field_name: field_user_research_groups
entity_type: user
bundle: user
label: 'Grupos de Pesquisa'
description: 'Grupos de pesquisa do usuário sincronizados do LDAP'
required: false
translatable: true
default_value: { }
default_value_callback: ''
settings:
handler: 'default:group'
handler_settings:
target_bundles:
research_group: research_group
sort:
field: _none
direction: ASC
auto_create: false
auto_create_bundle: ''
field_type: entity_reference

View File

@@ -0,0 +1,20 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_rg_code
field_name: field_rg_code
entity_type: group
type: string
settings:
max_length: 255
case_sensitive: false
is_ascii: false
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- user
id: group.field_rg_coord
field_name: field_rg_coord
entity_type: group
type: entity_reference
settings:
target_type: user
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- user
id: group.field_rg_coord_assoc
field_name: field_rg_coord_assoc
entity_type: group
type: entity_reference
settings:
target_type: user
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_rg_department
field_name: field_rg_department
entity_type: group
type: entity_reference
settings:
target_type: group
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,17 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_rg_mail
field_name: field_rg_mail
entity_type: group
type: email
settings: { }
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- telephone
id: group.field_rg_phone
field_name: field_rg_phone
entity_type: group
type: telephone
settings: { }
module: telephone
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- user
id: user.field_user_research_groups
field_name: field_user_research_groups
entity_type: user
type: entity_reference
settings:
target_type: group
module: core
locked: false
cardinality: -1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,37 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.research_group
id: research_group-admin
label: Admin
weight: 100
admin: true
scope: individual
global_role: null
group_type: research_group
permissions:
- administer members
- delete group
- edit group
- leave group
- view group
- view unpublished group
- access group_media overview
- access group_node overview
- create group_node:article entity
- create group_node:page entity
- delete any group_node:article entity
- delete any group_node:page entity
- delete own group_node:article entity
- delete own group_node:page entity
- update any group_node:article entity
- update any group_node:page entity
- update own group_node:article entity
- update own group_node:page entity
- view group_node:article entity
- view group_node:page entity
- view own unpublished group_node:article entity
- view own unpublished group_node:page entity
- view unpublished group_node:article entity
- view unpublished group_node:page entity

View File

@@ -0,0 +1,38 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.research_group
- user.role.administrator
id: research_group-admin_in
label: Administrador
weight: 102
admin: true
scope: insider
global_role: administrator
group_type: research_group
permissions:
- administer members
- delete group
- edit group
- leave group
- view group
- view unpublished group
- access group_media overview
- access group_node overview
- create group_node:article entity
- create group_node:page entity
- delete any group_node:article entity
- delete any group_node:page entity
- delete own group_node:article entity
- delete own group_node:page entity
- update any group_node:article entity
- update any group_node:page entity
- update own group_node:article entity
- update own group_node:page entity
- view group_node:article entity
- view group_node:page entity
- view own unpublished group_node:article entity
- view own unpublished group_node:page entity
- view unpublished group_node:article entity
- view unpublished group_node:page entity

View File

@@ -0,0 +1,38 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.research_group
- user.role.administrator
id: research_group-admin_out
label: Administrador
weight: 101
admin: true
scope: outsider
global_role: administrator
group_type: research_group
permissions:
- administer members
- delete group
- edit group
- join group
- view group
- view unpublished group
- access group_media overview
- access group_node overview
- create group_node:article entity
- create group_node:page entity
- delete any group_node:article entity
- delete any group_node:page entity
- delete own group_node:article entity
- delete own group_node:page entity
- update any group_node:article entity
- update any group_node:page entity
- update own group_node:article entity
- update own group_node:page entity
- view group_node:article entity
- view group_node:page entity
- view own unpublished group_node:article entity
- view own unpublished group_node:page entity
- view unpublished group_node:article entity
- view unpublished group_node:page entity

View File

@@ -0,0 +1,17 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.research_group
- user.role.anonymous
id: research_group-anonymous
label: 'Anônimo'
weight: -102
admin: false
scope: outsider
global_role: anonymous
group_type: research_group
permissions:
- view group
- view group_node:article entity
- view group_node:page entity

View File

@@ -0,0 +1,27 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.research_group
- user.role.authenticated
id: research_group-member
label: Member
weight: -100
admin: false
scope: insider
global_role: authenticated
group_type: research_group
permissions:
- view group
- access group_media overview
- access group_node overview
- create group_node:article entity
- create group_node:page entity
- delete own group_node:article entity
- delete own group_node:page entity
- update own group_node:article entity
- update own group_node:page entity
- view group_node:article entity
- view group_node:page entity
- view own unpublished group_node:article entity
- view own unpublished group_node:page entity

View File

@@ -0,0 +1,17 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.research_group
- user.role.authenticated
id: research_group-outsider
label: Outsider
weight: -101
admin: false
scope: outsider
global_role: authenticated
group_type: research_group
permissions:
- view group
- view group_node:article entity
- view group_node:page entity

View File

@@ -0,0 +1,10 @@
langcode: pt-br
status: true
dependencies: { }
id: research_group
label: 'Grupo de Pesquisa'
description: ''
new_revision: true
creator_membership: true
creator_wizard: true
creator_roles: { }

View File

@@ -0,0 +1,73 @@
/**
* Role mapping table styles
*/
/* Limita a largura da tabela e permite scroll horizontal se necessário */
.role-mappings-table {
max-width: 100%;
table-layout: fixed;
}
/* Define larguras fixas para cada coluna */
.role-mappings-table th:nth-child(1),
.role-mappings-table td:nth-child(1) {
width: 20%;
min-width: 150px;
}
.role-mappings-table th:nth-child(2),
.role-mappings-table td:nth-child(2) {
width: 15%;
min-width: 130px;
}
.role-mappings-table th:nth-child(3),
.role-mappings-table td:nth-child(3) {
width: 25%;
min-width: 180px;
}
.role-mappings-table th:nth-child(4),
.role-mappings-table td:nth-child(4) {
width: 30%;
min-width: 200px;
}
.role-mappings-table th:nth-child(5),
.role-mappings-table td:nth-child(5) {
width: 10%;
min-width: 80px;
text-align: center;
}
/* Estilo dos selects para limitar largura e truncar texto */
.role-mapping-field-select {
max-width: 100%;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Estilo para options dentro dos selects - mostra texto completo no dropdown */
.role-mapping-field-select option {
white-space: normal;
overflow: visible;
text-overflow: clip;
}
/* Estilo do textarea */
.role-mapping-values-textarea {
max-width: 100%;
width: 100%;
min-height: 50px;
}
/* Responsividade: em telas menores, permite scroll horizontal */
@media (max-width: 1200px) {
.role-mappings-table {
display: block;
overflow-x: auto;
white-space: nowrap;
}
}

View File

@@ -0,0 +1,13 @@
name: LDAP Research Groups Sync
type: module
description: 'Sincroniza grupos de pesquisa do servidor LDAP com grupos via cron'
version: '1.0.0'
core_version_requirement: ^11
package: Custom
dependencies:
- ldap_groups_sync:ldap_groups_sync
- drupal:options
- drupal:telephone
- group:group
- ldap:ldap_servers
- site_tools

View File

@@ -0,0 +1,94 @@
<?php
/**
* @file
* Arquivo de instalação para ldap_research_groups_sync.
*/
/**
* Implements hook_install().
*/
function ldap_research_groups_sync_install() {
\Drupal::messenger()->addWarning(t('Please ensure your group type has the required fields: field_rg_code, field_rg_phone, field_rg_mail, and field_rg_department (for department reference).'));
}
/**
* Adiciona os campos field_rg_coord e field_rg_coord_assoc ao bundle research_group.
*/
function ldap_research_groups_sync_update_10001() {
$configs = [
'field.storage.group.field_rg_coord',
'field.storage.group.field_rg_coord_assoc',
'field.field.group.research_group.field_rg_coord',
'field.field.group.research_group.field_rg_coord_assoc',
];
$module_path = \Drupal::service('extension.list.module')->getPath('ldap_research_groups_sync');
$config_path = $module_path . '/config/optional';
$config_factory = \Drupal::configFactory();
$yaml_parser = \Drupal::service('serialization.yaml');
$imported = 0;
foreach ($configs as $config_name) {
if (!$config_factory->get($config_name)->isNew()) {
continue;
}
$file = $config_path . '/' . $config_name . '.yml';
$data = $yaml_parser->decode(file_get_contents($file));
$config_factory->getEditable($config_name)->setData($data)->save();
$imported++;
}
if ($imported > 0) {
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
}
return t('Campos field_rg_coord e field_rg_coord_assoc importados (@count configs).', ['@count' => $imported]);
}
/**
* Adiciona o campo field_rg_code ao bundle research_group.
*/
function ldap_research_groups_sync_update_10002() {
$configs = [
'field.storage.group.field_rg_code',
'field.field.group.research_group.field_rg_code',
];
$module_path = \Drupal::service('extension.list.module')->getPath('ldap_research_groups_sync');
$config_path = $module_path . '/config/optional';
$config_factory = \Drupal::configFactory();
$yaml_parser = \Drupal::service('serialization.yaml');
$imported = 0;
foreach ($configs as $config_name) {
if (!$config_factory->get($config_name)->isNew()) {
continue;
}
$file = $config_path . '/' . $config_name . '.yml';
$data = $yaml_parser->decode(file_get_contents($file));
$config_factory->getEditable($config_name)->setData($data)->save();
$imported++;
}
if ($imported > 0) {
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
}
return t('Campo field_rg_code importado (@count configs).', ['@count' => $imported]);
}
/**
* Implements hook_uninstall().
*/
function ldap_research_groups_sync_uninstall() {
// Remove configurações
\Drupal::configFactory()->getEditable('ldap_research_groups_sync.settings')->delete();
\Drupal::messenger()->addStatus(t('LDAP Research Groups Sync module uninstalled.'));
}

View File

@@ -0,0 +1,5 @@
role_mapping_styles:
version: 1.x
css:
theme:
css/role-mapping.css: {}

View File

@@ -0,0 +1,6 @@
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

View File

@@ -0,0 +1,11 @@
ldap_research_groups_sync.tab.config:
title: 'Configuration'
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

View File

@@ -0,0 +1,238 @@
<?php
/**
* @file
* Module to synchronize research groups 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_research_groups_sync\LdapResearchGroupsSync;
/**
* Implements hook_help().
*/
function ldap_research_groups_sync_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.ldap_research_groups_sync':
return '<p>' . t('This module synchronizes research groups from an LDAP server to groups through cron.') . '</p>';
}
}
/**
* Implements hook_cron().
*/
function ldap_research_groups_sync_cron() {
// Check if module is properly configured
$config = \Drupal::config('ldap_research_groups_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_research_groups_sync')->warning('Synchronization cancelled: LDAP server "@server_id" not found or inactive. Configure at /admin/config/local-modules/ldap-research-groups-sync', [
'@server_id' => $ldap_server_id,
]);
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_research_groups_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_research_groups_sync')->warning('Synchronization cancelled: LDAP query "@query_id" not found or inactive. Configure at /admin/config/local-modules/ldap-research-groups-sync', [
'@query_id' => $ldap_query_id,
]);
return;
}
}
else {
\Drupal::logger('ldap_research_groups_sync')->warning('Synchronization cancelled: ldap_query module not available.');
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_research_groups_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_research_groups_sync')->warning('Synchronization cancelled: group type "@type_id" not found. Configure at /admin/config/local-modules/ldap-research-groups-sync', [
'@type_id' => $group_type_id,
]);
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_research_groups_sync')->warning('Synchronization cancelled: error checking group type: @message', [
'@message' => $e->getMessage(),
]);
return;
}
// Get LDAP synchronization service
$ldap_sync = \Drupal::service('ldap_research_groups_sync.sync');
// Execute research groups synchronization
try {
$ldap_sync->syncResearchGroups();
\Drupal::logger('ldap_research_groups_sync')->info('Research groups synchronization executed successfully.');
}
catch (\Exception $e) {
\Drupal::logger('ldap_research_groups_sync')->error('Error in research groups 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_research_groups_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_research_groups_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_research_groups_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_research_groups_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_research_groups: multi-value reference to the research groups
* 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_research_groups_sync_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
$protected_user_fields = [
'field_user_research_groups',
];
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 (LdapResearchGroupsSync::$syncing TRUE)
* - New user creation (initial provisioning via ldap_user)
* - Saves by users with the 'edit ldap managed user rg fields' permission
*/
function ldap_research_groups_sync_user_presave(\Drupal\user\UserInterface $user) {
// Permite durante qualquer sync LDAP autorizado.
if (LdapResearchGroupsSync::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 rg fields')) {
return;
}
$original = $user->original;
if ($original === NULL) {
return;
}
// Fields managed by LDAP that cannot be changed externally.
$protected_fields = [
'field_user_research_groups',
];
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_research_groups_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(),
]
);
}
}
}

View File

@@ -0,0 +1,9 @@
administer ldap research groups sync:
title: 'Administer LDAP Research Groups Sync'
description: 'Configure the LDAP research groups synchronization.'
restrict access: true
edit ldap managed user rg fields:
title: 'Edit LDAP-managed research group fields on users'
description: 'Allows manually editing fields such as field_user_research_groups that are normally managed by LDAP synchronization. Use only in emergencies.'
restrict access: true

View File

@@ -0,0 +1,25 @@
ldap_research_groups_sync.config:
path: '/admin/config/local-modules/ldap-research-groups-sync'
defaults:
_form: '\Drupal\ldap_research_groups_sync\Form\LdapResearchGroupsSyncConfigForm'
_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+'

View File

@@ -0,0 +1,8 @@
services:
ldap_research_groups_sync.sync:
class: Drupal\ldap_research_groups_sync\LdapResearchGroupsSync
arguments: ['@entity_type.manager', '@config.factory', '@logger.factory', '@ldap.bridge']
ldap_research_groups_sync.access_rules:
class: Drupal\ldap_groups_sync\GroupAccessRulesService
arguments: ['@config.factory', '@entity_type.manager', 'ldap_research_groups_sync.settings']

View File

@@ -0,0 +1,85 @@
<?php
/**
* @file
* Installs field_rg_department on the research_group group bundle.
*
* Usage (from Drupal root):
* drush php-script modules/custom/ldap_research_groups_sync/scripts/install_field_rg_department.php
*/
$module_path = \Drupal::service('extension.list.module')->getPath('ldap_research_groups_sync');
$config_path = $module_path . '/config/optional';
$config_factory = \Drupal::configFactory();
$yaml_parser = \Drupal::service('serialization.yaml');
$configs_to_install = [
'field.storage.group.field_rg_department',
'field.field.group.research_group.field_rg_department',
'core.entity_form_display.group.research_group.default',
'core.entity_view_display.group.research_group.default',
];
$imported = 0;
$skipped = 0;
foreach ($configs_to_install as $config_name) {
$file = $config_path . '/' . $config_name . '.yml';
if (!file_exists($file)) {
echo "ERROR: File not found: $file\n";
continue;
}
$existing = $config_factory->get($config_name);
// For display configs, always update (they already exist and need the new field).
$is_display = str_starts_with($config_name, 'core.entity_');
if (!$existing->isNew() && !$is_display) {
echo "SKIP: $config_name (already exists)\n";
$skipped++;
continue;
}
$data = $yaml_parser->decode(file_get_contents($file));
$config_factory->getEditable($config_name)->setData($data)->save();
echo "OK: $config_name\n";
$imported++;
}
if ($imported > 0) {
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
\Drupal::service('plugin.manager.field.widget')->clearCachedDefinitions();
\Drupal::service('plugin.manager.field.formatter')->clearCachedDefinitions();
\Drupal::service('config.factory')->clearStaticCache();
// Create the database table for the new field storage.
$field_manager = \Drupal::service('entity_field.manager');
$field_manager->clearCachedFieldDefinitions();
$update_manager = \Drupal::entityDefinitionUpdateManager();
$storage_defs = $field_manager->getFieldStorageDefinitions('group');
if (isset($storage_defs['field_rg_department'])) {
$update_manager->installFieldStorageDefinition(
'field_rg_department',
'group',
'ldap_research_groups_sync',
$storage_defs['field_rg_department']
);
echo "Schema update applied (group__field_rg_department table created).\n";
}
else {
echo "WARNING: field_rg_department storage definition not found — table not created.\n";
}
\Drupal::service('router.builder')->rebuild();
echo "\nDone. Imported: $imported, Skipped: $skipped\n";
echo "Run 'drush cr' to clear all caches.\n";
}
else {
echo "\nNothing imported. Skipped: $skipped\n";
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Drupal\ldap_research_groups_sync\Form;
use Drupal\ldap_groups_sync\Form\AccessRuleFormBase;
/**
* Modal form for creating or editing a single access rule (Research Groups Sync).
*/
class AccessRuleForm extends AccessRuleFormBase {
/**
* {@inheritdoc}
*/
protected function getConfigName(): string {
return 'ldap_research_groups_sync.settings';
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'ldap_research_groups_sync_access_rule_form';
}
/**
* {@inheritdoc}
*/
protected function getAccessRulesRoute(): string {
return 'ldap_research_groups_sync.access_rules';
}
/**
* {@inheritdoc}
*/
protected function getDefaultGroupTypeId(): string {
return 'research_group';
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Drupal\ldap_research_groups_sync\Form;
use Drupal\ldap_groups_sync\Form\AccessRulesFormBase;
/**
* Lists and manages access rules for the LDAP Research Groups Sync module.
*/
class AccessRulesForm extends AccessRulesFormBase {
/**
* {@inheritdoc}
*/
protected function getConfigName(): string {
return 'ldap_research_groups_sync.settings';
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'ldap_research_groups_sync_access_rules_form';
}
/**
* {@inheritdoc}
*/
protected function getAccessRuleFormRoute(): string {
return 'ldap_research_groups_sync.access_rule_form';
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\ldap_research_groups_sync\Plugin\EntityReferenceSelection;
use Drupal\group\Plugin\EntityReferenceSelection\GroupSelection;
/**
* Plugin for research group entity reference selection.
*
* Restricts selection to the 'research_group' group type.
*
* @EntityReferenceSelection(
* id = "default:research_group",
* label = @Translation("Research Group selection"),
* entity_types = {"group"},
* group = "default",
* weight = 1
* )
*/
class ResearchGroupSelection extends GroupSelection {
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
$query->condition('type', 'research_group');
return $query;
}
}

View File

@@ -0,0 +1,298 @@
# Portuguese (Brazil) translation for LDAP Research Groups Sync module
# Copyright (c) 2026
# This file is distributed under the same license as the LDAP Research Groups Sync module.
#
msgid ""
msgstr ""
"Project-Id-Version: LDAP Research Groups Sync 1.0.0\n"
"POT-Creation-Date: 2026-02-27 00:00+0000\n"
"PO-Revision-Date: 2026-02-27 00:00+0000\n"
"Language-Team: Portuguese (Brazil)\n"
"Language: pt-br\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: ldap_research_groups_sync.routing.yml
msgid "LDAP Research Groups Sync"
msgstr "Sincronização de Grupos de Pesquisa LDAP"
#: ldap_research_groups_sync.routing.yml
msgid "Access Rules"
msgstr "Regras de Acesso"
#: ldap_research_groups_sync.routing.yml
msgid "Access Rule"
msgstr "Regra de Acesso"
#: ldap_research_groups_sync.module
msgid "This module synchronizes research groups from an LDAP server to groups through cron."
msgstr "Este módulo sincroniza grupos de pesquisa de um servidor LDAP para grupos através do cron."
#: ldap_research_groups_sync.install
msgid "Please ensure your group type has the required fields: field_rg_code, field_rg_phone, field_rg_mail, and field_rg_department (for department reference)."
msgstr "Certifique-se de que o tipo de grupo possui os campos obrigatórios: field_rg_code, field_rg_phone, field_rg_mail e field_rg_department (para referência de departamento)."
msgid "LDAP Research Groups Sync module uninstalled."
msgstr "Módulo LDAP Research Groups Sync desinstalado."
#: src/Form/LdapResearchGroupsSyncConfigForm.php
msgid "Group Type"
msgstr "Tipo de Grupo"
msgid "Target Group Type"
msgstr "Tipo de Grupo Alvo"
msgid "Select the group type where research groups will be synchronized. <br><strong>Required fields:</strong> field_rg_code, field_rg_phone, field_rg_mail"
msgstr "Selecione o tipo de grupo onde os grupos de pesquisa serão sincronizados. <br><strong>Campos obrigatórios:</strong> field_rg_code, field_rg_phone, field_rg_mail"
msgid "- Select a group type -"
msgstr "- Selecione um tipo de grupo -"
msgid "The default \"research_group\" group type is not installed."
msgstr "O tipo de grupo padrão \"research_group\" não está instalado."
msgid "Install Default Configuration"
msgstr "Instalar Configuração Padrão"
msgid "The button above will create:<ul>\n <li>Group type \"research_group\" with all required fields</li>\n <li>User fields for research group synchronization</li>\n <li>Default group roles and displays</li>\n </ul>Or you can <a href=\"@url\">create a custom group type</a> manually."
msgstr "O botão acima irá criar:<ul>\n <li>Tipo de grupo \"research_group\" com todos os campos obrigatórios</li>\n <li>Campos de usuário para sincronização de grupo de pesquisa</li>\n <li>Papéis e exibições padrão do grupo</li>\n </ul>Ou você pode <a href=\"@url\">criar um tipo de grupo personalizado</a> manualmente."
msgid "Create New Group Type"
msgstr "Criar Novo Tipo de Grupo"
msgid "Manage Group Type"
msgstr "Gerenciar Tipo de Grupo"
msgid "Manage Fields"
msgstr "Gerenciar Campos"
msgid "LDAP Server"
msgstr "Servidor LDAP"
msgid "Select the LDAP server configured for research groups synchronization."
msgstr "Selecione o servidor LDAP configurado para sincronização de grupos de pesquisa."
msgid "- Select a server -"
msgstr "- Selecione um servidor -"
msgid "No LDAP server found. <a href=\"@url\">Configure an LDAP server</a> first."
msgstr "Nenhum servidor LDAP encontrado. <a href=\"@url\">Configure um servidor LDAP</a> primeiro."
msgid "LDAP Query"
msgstr "Consulta LDAP"
msgid "Select the LDAP query that will be used to search for research groups."
msgstr "Selecione a consulta LDAP que será usada para buscar grupos de pesquisa."
msgid "- Select a query -"
msgstr "- Selecione uma query -"
msgid "No LDAP query found for the selected server. <a href=\"@url\">Create a new LDAP query</a>."
msgstr "Nenhuma query LDAP encontrada para o servidor selecionado. <a href=\"@url\">Crie uma nova query LDAP</a>."
msgid "Create New LDAP Query"
msgstr "Criar Nova Query LDAP"
msgid "Edit Selected Query"
msgstr "Editar Query Selecionada"
msgid "Attribute Mapping"
msgstr "Mapeamento de Atributos"
msgid "Configure how LDAP attributes are mapped to target entity fields.<br><strong>Note:</strong> For \"User Reference\" mappings, the search attribute will be automatically obtained from the \"Account name attribute\" field configured in the selected LDAP server."
msgstr "Configure como os atributos LDAP são mapeados para os campos da entidade alvo.<br><strong>Nota:</strong> Para mapeamentos do tipo \"Referência de Usuário\", o atributo de busca será obtido automaticamente do campo \"Atributo de nome de conta\" configurado no servidor LDAP selecionado."
msgid "Entity Field"
msgstr "Campo da Entidade"
msgid "LDAP Attribute"
msgstr "Atributo LDAP"
msgid "Mapping Type"
msgstr "Tipo de Mapeamento"
msgid "Remove"
msgstr "Remover"
msgid "No mappings configured."
msgstr "Nenhum mapeamento configurado."
msgid "- Select a field -"
msgstr "- Selecione um campo -"
msgid "- Select an attribute -"
msgstr "- Selecione um atributo -"
msgid "Simple (text)"
msgstr "Simples (texto)"
msgid "User Reference (DN → User)"
msgstr "Referência de Usuário (DN → Usuário)"
msgid "Department Reference (code → Department)"
msgstr "Referência de Departamento (código → Departamento)"
msgid "Add Mapping"
msgstr "Adicionar Mapeamento"
msgid "Remove Selected"
msgstr "Remover Selecionados"
msgid "Member Synchronization"
msgstr "Sincronização de Membros"
msgid "Enable member synchronization from LDAP attribute"
msgstr "Habilitar sincronização de membros a partir do atributo LDAP"
msgid "When enabled, group memberships are managed based on the LDAP member attribute of each group entry. Members are added or removed to match the LDAP list."
msgstr "Quando habilitado, as associações de membros do grupo são gerenciadas com base no atributo de membros LDAP de cada entrada de grupo. Membros são adicionados ou removidos para corresponder à lista LDAP."
msgid "Member LDAP attribute"
msgstr "Atributo LDAP de membros"
msgid "Name of the LDAP attribute on each group entry that lists its members (as DNs or UIDs). Common values: <code>member</code>, <code>uniqueMember</code>, <code>memberUid</code>. The attribute must be included in the LDAP query."
msgstr "Nome do atributo LDAP em cada entrada de grupo que lista seus membros (como DNs ou UIDs). Valores comuns: <code>member</code>, <code>uniqueMember</code>, <code>memberUid</code>. O atributo deve estar incluído na query LDAP."
msgid "Group Role Mapping"
msgstr "Mapeamento de Papéis do Grupo"
msgid "Configure how user attributes map to group roles. Each role can have its own mapping criteria."
msgstr "Configure como os atributos do usuário são mapeados para papéis do grupo. Cada papel pode ter seus próprios critérios de mapeamento."
msgid "Enable role mapping"
msgstr "Habilitar mapeamento de papéis"
msgid "When enabled, users will be assigned group roles based on the mappings below."
msgstr "Quando habilitado, os usuários receberão papéis de grupo com base nos mapeamentos abaixo."
msgid "Group Role"
msgstr "Papel do Grupo"
msgid "Source"
msgstr "Fonte"
msgid "User Field / LDAP Attribute"
msgstr "Campo do Usuário / Atributo LDAP"
msgid "Group Field / Values"
msgstr "Campo do Grupo / Valores"
msgid "No role mappings configured. Users will be assigned the default role."
msgstr "Nenhum mapeamento de papel configurado. Os usuários receberão o papel padrão."
msgid "- Select a role -"
msgstr "- Selecione um papel -"
msgid "User Field"
msgstr "Campo do Usuário"
msgid "LDAP Attribute"
msgstr "Atributo LDAP"
msgid "Group Field Match"
msgstr "Correspondência de Campo do Grupo"
msgid "- Select user field -"
msgstr "- Selecione campo do usuário -"
msgid "- Select attribute/field -"
msgstr "- Selecione atributo/campo -"
msgid "- Select group field -"
msgstr "- Selecione campo do grupo -"
msgid "e.g., Director, Manager"
msgstr "ex: Diretor, Gerente"
msgid "Add Role Mapping"
msgstr "Adicionar Mapeamento de Papel"
msgid "Save Configuration"
msgstr "Salvar Configuração"
msgid "Test Connection"
msgstr "Testar Conexão"
msgid "Synchronize Research Groups"
msgstr "Sincronizar Grupos de Pesquisa"
msgid "Error loading LDAP servers: @message"
msgstr "Erro ao carregar servidores LDAP: @message"
msgid "Error loading query attributes: @message"
msgstr "Erro ao carregar atributos da query: @message"
msgid "LDAP attribute is required when field is selected."
msgstr "O atributo LDAP é obrigatório quando um campo é selecionado."
msgid "Vocabulary field is required when attribute is selected."
msgstr "O campo vocabulário é obrigatório quando um atributo é selecionado."
msgid "Configuration saved successfully."
msgstr "Configuração salva com sucesso."
msgid "Please select an LDAP server to test."
msgstr "Por favor, selecione um servidor LDAP para testar."
msgid "LDAP server \"@server\" is configured and active."
msgstr "Servidor LDAP \"@server\" está configurado e ativo."
msgid "LDAP server not found or inactive."
msgstr "Servidor LDAP não encontrado ou inativo."
msgid "Error testing connection: @message"
msgstr "Erro ao testar conexão: @message"
msgid "Please fix the errors in the form before running the synchronization."
msgstr "Corrija os erros no formulário antes de executar a sincronização."
msgid "Synchronization completed successfully. Check the logs for details."
msgstr "Sincronização executada com sucesso. Verifique os logs para detalhes."
msgid "Error during synchronization: @message"
msgstr "Erro durante a sincronização: @message"
msgid "Internal"
msgstr "Interno"
msgid "Outsider"
msgstr "Externo"
msgid "Error loading group roles: @message"
msgstr "Erro ao carregar papéis do grupo: @message"
msgid "Error loading user fields: @message"
msgstr "Erro ao carregar campos do usuário: @message"
msgid "The ldap_query module is not available. LDAP queries cannot be loaded."
msgstr "O módulo ldap_query não está disponível. As queries LDAP não podem ser carregadas."
msgid "Error loading LDAP queries: @message"
msgstr "Erro ao carregar queries LDAP: @message"
msgid "Label (group title)"
msgstr "Rótulo (título do grupo)"
msgid "Group type \"@type\" not found. Please create it first or select a different group type."
msgstr "Tipo de grupo \"@type\" não encontrado. Crie-o primeiro ou selecione um tipo de grupo diferente."
msgid "Error loading group types: @message"
msgstr "Erro ao carregar tipos de grupo: @message"
msgid "Configuration directory not found: @path"
msgstr "Diretório de configuração não encontrado: @path"
msgid "Configuration @name already exists, skipping."
msgstr "Configuração @name já existe, pulando."
msgid "Successfully imported @count configuration files."
msgstr "Importados com sucesso @count arquivos de configuração."
msgid "No new configuration files were imported. All configurations already exist."
msgstr "Nenhum arquivo de configuração novo foi importado. Todas as configurações já existem."
msgid "Error importing configuration: @message"
msgstr "Erro ao importar configuração: @message"

Some files were not shown because too many files have changed in this diff Show More