You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Missing group members after oc10-to-oCIS migration #18
There is an issue with the group members in the details view after successful migration from oc10 to oCIS.
Group members are not displayed in the Members section of the details view for a selected group.
See the attached screenshots and recorded video.
🤖 Automated issue triage by Claude Code review agent.
Thanks for the detailed report and screenshots. Confirmed as a data-integrity bug in the group-member migration. Here is the analysis with the located cause.
Where members get dropped
Member assignment happens in lib/MigrationState/StateMigrateGroups.php → migrateGroup(). For every member of an oC10 group it resolves the oCIS user id and only then adds the membership:
So a member is added only if UserGroupFinder::getUser() returns a non-null oCIS id. When it returns null, the member is skipped and the group ends up empty — matching the "No members found" screenshots.
Root cause: the cross-command cache load is broken
getUser() (lib/Helper/UserGroupFinder.php) first consults $userCache (oc10 username → oCIS id). That cache is populated and written to disk during the users step (StateMigrateUsers::doMigrate → addUserToCache() + saveCache()), then re-read at the start of the groups step via loadCache().
loadCache() is buggy:
$filePointer = \fopen($targetFile, 'r'); // opened READ-ONLYif (!\flock($filePointer, LOCK_EX)) { ... }
\ftruncate($filePointer, 0); // truncates the cache to 0 bytes$content = \fread($filePointer, \filesize($targetFile)); // filesize now 0 -> reads nothing...$data = \json_decode($content, true);
if (!\is_array($data)) {
thrownew \UnexpectedValueException(...); // load fails
}
The function opens the file read-only and then calls ftruncate(...,0) on it (a stray copy-paste of the saveCache truncate logic). The cache content is destroyed / never read, json_decode yields null, and UnexpectedValueException is thrown. In StateMigrateGroups::doMigrate() that exception is caught and swallowed:
try {
$this->userGroupFinder->loadCache();
} catch (\UnexpectedValueException$ex) {
$output->writeln("<comment>Cache for the UserGroupFinder couldn't be loaded ...</comment>");
// we can keep going, albeit slowly
}
So the group step always starts with an empty user cache.
Why the live fallback then also misses members
With an empty cache, getUser() falls back to a live lookup: Client::checkUser() → searchUsers(), which queries GET /graph/v1.0/users?$search="username" and requires an exact onPremisesSamAccountName match. This fallback is fragile right after a bulk user import:
the oCIS $search index is eventually-consistent and may not yet return users just created in the preceding step, and/or
$search term/encoding semantics may not return an exact hit for every username.
Any miss returns null → the member is silently SKIPPED. Because the cache (the reliable path) never loads, every membership depends on this fragile search, which explains members disappearing wholesale rather than occasionally.
Suggested fix direction
Fix loadCache(): open the file readable and do not truncate it (drop the ftruncate call; read with the real file size, ideally file_get_contents/stream_get_contents rather than fread(..., filesize())). This alone restores the reliable username→id mapping and should resolve most cases.
Harden the live fallback: verify the $search result truly matches before treating it as "not found", and consider retry/backoff or a deterministic lookup (filter by onPremisesSamAccountName) instead of $search, to cover eventual-consistency.
Make a SKIPPED member a loud, summarized warning (and ideally a non-zero/auditable outcome) rather than a single log line, so silent membership loss is surfaced during migration.
To confirm on your side
When reproducing, the migration groups step output is the smoking gun:
a Cache for the UserGroupFinder couldn't be loaded: ... comment line at the start, and
per-member <group> <user> SKIPPED lines instead of ... added.
If you can attach that migrate:to-ocis:migrate:groups console output it will confirm which of the two paths (cache load vs. search fallback) failed in your run.
Activity
DeepDiver1975 commentedon Jun 24, 2026
🤖 Automated issue triage by Claude Code review agent.
Thanks for the detailed report and screenshots. Confirmed as a data-integrity bug in the group-member migration. Here is the analysis with the located cause.
Where members get dropped
Member assignment happens in
lib/MigrationState/StateMigrateGroups.php→migrateGroup(). For every member of an oC10 group it resolves the oCIS user id and only then adds the membership:So a member is added only if
UserGroupFinder::getUser()returns a non-null oCIS id. When it returns null, the member is skipped and the group ends up empty — matching the "No members found" screenshots.Root cause: the cross-command cache load is broken
getUser()(lib/Helper/UserGroupFinder.php) first consults$userCache(oc10 username → oCIS id). That cache is populated and written to disk during the users step (StateMigrateUsers::doMigrate→addUserToCache()+saveCache()), then re-read at the start of the groups step vialoadCache().loadCache()is buggy:The function opens the file read-only and then calls
ftruncate(...,0)on it (a stray copy-paste of thesaveCachetruncate logic). The cache content is destroyed / never read,json_decodeyields null, andUnexpectedValueExceptionis thrown. InStateMigrateGroups::doMigrate()that exception is caught and swallowed:So the group step always starts with an empty user cache.
Why the live fallback then also misses members
With an empty cache,
getUser()falls back to a live lookup:Client::checkUser()→searchUsers(), which queriesGET /graph/v1.0/users?$search="username"and requires an exactonPremisesSamAccountNamematch. This fallback is fragile right after a bulk user import:$searchindex is eventually-consistent and may not yet return users just created in the preceding step, and/or$searchterm/encoding semantics may not return an exact hit for every username.Any miss returns null → the member is silently SKIPPED. Because the cache (the reliable path) never loads, every membership depends on this fragile search, which explains members disappearing wholesale rather than occasionally.
Suggested fix direction
loadCache(): open the file readable and do not truncate it (drop theftruncatecall; read with the real file size, ideallyfile_get_contents/stream_get_contentsrather thanfread(..., filesize())). This alone restores the reliable username→id mapping and should resolve most cases.$searchresult truly matches before treating it as "not found", and consider retry/backoff or a deterministic lookup (filter byonPremisesSamAccountName) instead of$search, to cover eventual-consistency.To confirm on your side
When reproducing, the migration
groupsstep output is the smoking gun:Cache for the UserGroupFinder couldn't be loaded: ...comment line at the start, and<group> <user> SKIPPEDlines instead of... added.If you can attach that
migrate:to-ocis:migrate:groupsconsole output it will confirm which of the two paths (cache load vs. search fallback) failed in your run.