Skip to content

fix: scan each owner with its own credentials, not the previous owner's [patch] - #429

Merged
matt-edmondson merged 2 commits into
mainfrom
fix/426-reset-credentials-per-owner
Sep 22, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
fix/426-reset-credentials-per-owner

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #426

What was wrong

GitHubClient.Credentials is one mutable property on a client shared by every owner in the scan. ScanRemoteAccountsForRepos only assigned it when the owner had a PAT, or when a global login existed:

if (!string.IsNullOrEmpty(pat) || (!string.IsNullOrEmpty(Options.GitHubLogin) && !string.IsNullOrEmpty(Options.GitHubToken)))
{
    GitHubClient.Credentials = !string.IsNullOrEmpty(pat) ? new(owner, pat) : new(Options.GitHubLogin, Options.GitHubToken);
}

An owner matching neither branch does not get scanned anonymously — it gets scanned as whoever was set last.

The concrete case from the issue: a personal PAT is configured for owner A, and owner B is added with no credentials, just to browse publicly. B's scan runs as A. B's own private repositories are missed, and anything that genuinely needs B's auth answers with an ApiException that the caller swallows at lines 777-780 — so the repositories are simply missing, with no indication why.

The fix

ChooseCredentials makes the answer total, which is the property the old code lacked:

Owner PAT Global login + token Result
set either Credentials(owner, pat)
empty both set Credentials(login, token)
empty either missing Credentials.Anonymous

The caller now assigns its result for every owner, so nothing can carry over.

The third row also closes a smaller gap: a global login missing its token, or a token missing its login, used to be excluded by the guard and so silently left the previous credentials in place. It now falls through to anonymous rather than reaching Octokit, which rejects an empty half.

Tests

ProjectDirector.Test/ScanCredentialTests.cs, following the pattern CLAUDE.md sets out — the part with a rule in it is a plain method, drivable without a live ImGui context or a GitHub account.

AnOwnerWithNoCredentialsAnywhereIsScannedAnonymously is the regression itself: it asks for owner A with a PAT and then owner B with nothing, and asserts B comes back anonymous and specifically not as alpha.

Verified by substituting the original behaviour back in (returning the last-set credentials for an owner that matched neither branch) and re-running: AnOwnerWithNoCredentialsAnywhereIsScannedAnonymously, AGlobalLoginMissingItsTokenIsNotUsed and AGlobalTokenMissingItsLoginIsNotUsed all fail against it and pass against the fix.

Full suite on this branch: 45 total, 43 passed, 0 failed, 2 inconclusive (both need git-lfs, unrelated and present on main). dotnet build --configuration Release is clean.

Scope

The constructor's own one-time GitHubClient.Credentials assignment (lines 93-96) is left alone — a freshly constructed client is anonymous, so there is no stale identity for it to leak.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EdV5iCFkUxFqkLZQAGLAVT


Generated by Claude Code

…'s [patch]

GitHubClient.Credentials is one mutable property on a client shared by
every owner in the scan, and ScanRemoteAccountsForRepos only assigned it
when the owner had a PAT or a global login existed. An owner with
neither left the previous owner's credentials in place and was scanned
as them.

The concrete case: a personal PAT is configured for owner A, and owner B
is added with no credentials to browse publicly. B is then scanned as A,
so B's own private repositories are missed, and anything that genuinely
needed B's auth answers with an ApiException that the caller swallows --
leaving the repositories missing with no indication why.

Pull the choice out into ChooseCredentials, which is total: the owner's
own token, else the global login, else Credentials.Anonymous. The caller
assigns its result for every owner, so nothing can carry over. A global
login missing its token, or a token missing its login, now falls through
to anonymous rather than reaching Octokit, which rejects an empty half.

Fixes #426

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EdV5iCFkUxFqkLZQAGLAVT
The SonarCloud gate failed at 75% coverage on new code, against a
required 80%. The uncovered line was the one that mattered most: the
assignment in ScanRemoteAccountsForRepos, which is what actually stops
the previous owner's identity carrying over. ChooseCredentials was fully
covered, but the rule only helps if the caller applies it for every
owner, and nothing exercised that.

Move the assignment into ApplyCredentials so a test can watch one client
across two owners -- the shape the defect actually had. Scanning A with
a PAT and then B with nothing now asserts against a real GitHubClient
that B is anonymous rather than still authenticated as A.

Verified by restoring the conditional assignment in ApplyCredentials:
ScanningASecondOwnerDoesNotInheritTheFirstOwnersCredentials fails
against it and passes against the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EdV5iCFkUxFqkLZQAGLAVT
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 4bb70f4 into main Sep 22, 2026
17 of 18 checks passed
@matt-edmondson
matt-edmondson deleted the fix/426-reset-credentials-per-owner branch September 22, 2026 00:19
matt-edmondson pushed a commit that referenced this pull request Sep 22, 2026
Brings in #429 and #430, both of which changed ProjectDirector.cs, so the
combination is built and tested here rather than assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EdV5iCFkUxFqkLZQAGLAVT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GitHub credentials from one owner leak into the scan of the next, misdirecting private-repo lookups

2 participants