Skip to content

Missing group members after oc10-to-oCIS migration #18

Open

Description

@deyankiteworks

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.

Screenshots:

Image Image Image

Steps to reproduce:

  1. Following the Regular Migration Guide to migrate users and groups from oc10 to oCIS
  2. Execute the Migration Steps from Regular Migration Guide
  3. Login to oCIS with admin user
  4. Open Admins Settings page
  5. Go to Groups
  6. Open the details view for a group with some members in it (Members > 1)
  7. In the details view go to Members section
  8. Observe "No members found" label is displayed.

Actual behavior:

After successful migration "No members found" label is displayed in the details view, Members tab page.

Expected behavior:

After successful migration a list with group users should be displayed in the details view

Activity

  1. DeepDiver1975 commented on Jun 24, 2026

    @DeepDiver1975
    Contributor

    🤖 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:

    foreach ($group->getUsers() as $user) {
        $ocisUserId = $this->userGroupFinder->getUser($adminUser, $token, $user);
        if ($ocisUserId === null) {
            $output->writeln("  {$group->getDisplayName()} {$username} <error>SKIPPED</error>");
            continue;   // <-- member silently dropped
        }
        $client->addMemberToGroup($adminUser, $token, $groupBody['id'], $ocisUserId);
    }

    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-ONLY
    if (!\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)) {
        throw new \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

    1. 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.
    2. 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.
    3. 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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions