Skip to content

fix: Fix issues with replicating users#23150

Open
jason-p-pickering wants to merge 12 commits intomasterfrom
fix-replicate-users
Open

fix: Fix issues with replicating users#23150
jason-p-pickering wants to merge 12 commits intomasterfrom
fix-replicate-users

Conversation

@jason-p-pickering
Copy link
Contributor

@jason-p-pickering jason-p-pickering commented Mar 6, 2026

Root Cause

replicateUser used DefaultMetadataMergeService.merge() to clone the source user. The merge service guards every collection copy with Hibernate.isInitialized() - silently skipping any collection that isn't already loaded in memory. Since the source User arrives from the controller as a detached entity, all lazy collections (roles, groups, org units, dimension constraints) are uninitialized, and the replica was created with none of them. This was noted due to failing tracker performance tests, and not DHIS2 integration tests.

A secondary issue: owning-side collections (userRoles, organisationUnits, dataViewOrganisationUnits, teiSearchOrganisationUnits, cogsDimensionConstraints,catDimensionConstraints) written via raw JDBC after session.persist() are invisible to the L1 cache without explicit eviction, causing stale reads immediately after replication.

Solution

  • Re-fetch the source user inside the @transactional boundary via userStore.getByUidNoAcl() to ensure collections are accessible without loading them into memory
  • Copy all collection memberships via direct JDBC bulk INSERT … SELECT … WHERE NOT EXISTS - avoids touching any Java collection, which could trigger Hibernate lazy-loading the full collection on the inverse side (orgunits, groups, roles)
  • Call entityManager.flush() before JDBC so the new userinfo row is visible to the DB
  • Evict L1 and L2 caches for all owning-side collections on the replica after JDBC writes
  • Reject replication of externalAuth=true users (409 Conflict) - a cloned account with no provider link is broken by construction
  • Clear security-sensitive fields on the replica: secret, twoFactorType, restoreToken, restoreExpiry, idToken

Changes

  • DefaultUserService.replicateUser: full rewrite of collection copy logic
  • UserStore / HibernateUserStore: new copyOrgUnitMemberships and copyDimensionConstraints methods
  • UserRoleStore / HibernateUserRoleStore: new addMember method for JDBC role assignment
  • UserControllerTest: added testReplicateUserCopiesRolesGroupsOrgUnitsAndConstraints and testReplicateUserWithExternalAuthIsRejected

Known Limitations

Distributed cache (Redis): The post-JDBC cache eviction covers the Hibernate L1 session cache and the local L2 second-level cache. If a Redis-backed distributed cache is in use for user collection regions, those entries are not invalidated here. A subsequent read from a different node may return stale membership data until the TTL expires.

Temporary workaround: This PR works around the root cause rather than eliminating it. The correct long-term fix is to replace the metadataMergeService.merge() call with a dedicated User.copyScalarFieldsFrom(User source) method that makes the clone contract explicit - only copying fields that are safe and meaningful to replicate, with no
dependence on Hibernate initialization state.

@jason-p-pickering jason-p-pickering added run-api-tests This label will trigger an api-test job for the PR. run-perf-tests Enables performance tests labels Mar 6, 2026
@jason-p-pickering jason-p-pickering changed the title fix: Fix issue with replicating users fix: Fix issues with replicating users Mar 6, 2026
@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 6, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-api-tests This label will trigger an api-test job for the PR. run-perf-tests Enables performance tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant