[OP-19185] Extend LDAP sync to build org structure (department sync)#23848
Merged
Conversation
Generated by 🚫 Danger |
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
Member
e454f4a to
89eba77
Compare
Member
…he group to the root
…when unlinking an LDAP sync
oliverguenther
approved these changes
Jun 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Implements OP-19185.
What this does
Adds LDAP organizational unit → Department synchronization, analogous to the existing flat LDAP group sync. Given a configured base DN, OpenProject walks the OU subtree beneath it, mirrors it into the Department hierarchy, and assigns each user to the Department of the OU it resides in (derived from the user's DN). This realizes the ticket's two acceptance criteria — sync can produce departments (alongside the existing groups path) and the result is nested.
New module:
modules/ldap_departmentsMirrors
modules/ldap_groups. Enterprise-gated behind the existing:ldap_groupsfeature; admin UI additionally behind thedepartments_active?runtime flag.SynchronizedTree(base-DN config: base DN, OU structure filter, OU-name attribute, optional stable-ID attribute, optional user filter,sync_users),SynchronizedDepartment(OU↔Department mapping),Membership(sync-managed user↔department).SynchronizeTreeService: discovers OUs, builds the Department tree parents-first, matches by stable GUID with DN fallback (so OU renames/moves don't orphan), prunes vanished OUs (keeps the Department, drops the mapping).SynchronizeMembersService: places users by parsing each entry's parent container DN, LDAP-authoritative single-department moves, removes memberships the sync no longer sees, optional user creation.SynchronizationServiceorchestrator +SynchronizationJob(cron*/30, separate from group sync).SynchronizedTreesController(CRUD + on-demand synchronize),SynchronizedDepartmentsController(unmanage a mapping), views, i18n.ldap_departments_disable_sync_jobsetting; module registered inGemfile.modules.Core changes (main app)
users[lastname,type]unique index to placeholder users;Group#uniqueness_of_nameis now OU-aware (global for regular groups, per-parent for organizational units). Real OU trees repeat names (Supportunder both IT and HR), which the previous global constraint forbade.Group#ldap_managed?(a registration hook so core stays module-agnostic) gatesGroups::BaseContract/DeleteContract; the sync uses the permissiveGroups::SyncUpdateContract. The admin UI hides rename/re-parent/add/remove/delete actions and shows a "Managed by LDAP synchronization" badge. Departments become editable again only once the OU disappears and the mapping is dropped.Tests & dev tooling
spec/fixtures/ldap/users.ldifwith a nested OU tree (ou=org) + users — additive, existingldap_groupsspecs still pass.ldap_departments:development:ldap_serverrake task boots a dev LDAP server, configures a tree, runs the sync, and prints the resulting structure. The shared fixture means the existingldap_groups:development:ldap_servernow also exposes the OUs.Decisions worth reviewer attention
Manual verification
./bin/rails ldap_departments:development:ldap_server