Files
orca/src/main/github/github-enterprise-repository.test.ts
Brennan Benson 4b0e01f613 fix(github): scope GHES host-auth cache to the executing connection (#14948)
* fix(github): scope GHES host-auth cache to the executing connection

The gh auth answer cached for a connection-backed repo is probed without
the repository cwd, so it describes that connection's runtime — not the
local host. Keying only on repoPath+wslDistro let a local repo and an
SSH-hosted repo at the same path share one entry, and whichever resolved
first decided "is this GHES host authenticated" for both.

Include the connection identity in the runtime cache key. Local and WSL
keys are unchanged.

* fix(github): fence GHES auth cache across SSH reconnects

* fix(github): fence origin cache across SSH reconnects

* fix(github): fence repository identity cache on reconnect
2026-08-18 13:18:56 -07:00

591 lines
20 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const { commandExecFileAsyncMock, ghExecFileAsyncMock, gitExecFileAsyncMock, resolveWithSshGMock } =
vi.hoisted(() => ({
commandExecFileAsyncMock: vi.fn(),
ghExecFileAsyncMock: vi.fn(),
gitExecFileAsyncMock: vi.fn(),
resolveWithSshGMock: vi.fn()
}))
// Mock only the exec boundary so the real remote-identity parsing, runtime
// option resolution, and `gh auth status` parsing run against controlled output.
vi.mock('../git/runner', () => ({
commandExecFileAsync: commandExecFileAsyncMock,
ghExecFileAsync: ghExecFileAsyncMock,
gitExecFileAsync: gitExecFileAsyncMock
}))
vi.mock('../ssh/ssh-g-config-resolution', () => ({
resolveWithSshG: resolveWithSshGMock
}))
import {
_resetGitHubHostAuthCache,
getEnterpriseGitHubRepoSlug,
isGitHubHostAuthenticated,
isGitHubHostAuthenticatedForGlobalCli
} from './github-enterprise-repository'
import { _resetSshHostnameResolutionCache } from './github-ssh-host-alias-resolution'
import { registerSshGitProvider, unregisterSshGitProvider } from '../providers/ssh-git-dispatch'
function mockOriginRemote(url: string): void {
gitExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (args[0] === 'remote' && args[1] === 'get-url') {
return { stdout: `${url}\n`, stderr: '' }
}
return { stdout: '', stderr: '' }
})
}
function sshConfig(hostname: string, port = 22) {
return {
hostname,
port,
identityFile: [],
identitiesOnly: false,
forwardAgent: false,
proxyUseFdpass: false,
controlMaster: 'no',
controlPersist: 'no'
}
}
// gh auth status inventory entries represent hosts with configured credentials.
function mockHostAuthenticated(host = 'github.acme-corp.com'): void {
mockAuthenticatedHosts([host])
}
function mockAuthenticatedHosts(hosts: string[]): void {
ghExecFileAsyncMock.mockResolvedValue({
stdout: hosts
.map((host) => `${host}\n ✓ Logged in to ${host} account kelora (keyring)`)
.join('\n'),
stderr: ''
})
}
// gh exits non-zero and reports no matching host when not logged in.
function mockHostNotAuthenticated(): void {
ghExecFileAsyncMock.mockRejectedValue(
Object.assign(new Error('exit 1'), {
stdout: '',
stderr: 'You are not logged into any GitHub hosts. To log in, run: gh auth login'
})
)
}
describe('getEnterpriseGitHubRepoSlug', () => {
beforeEach(() => {
commandExecFileAsyncMock.mockReset()
ghExecFileAsyncMock.mockReset()
gitExecFileAsyncMock.mockReset()
resolveWithSshGMock.mockReset()
resolveWithSshGMock.mockResolvedValue(null)
_resetGitHubHostAuthCache()
_resetSshHostnameResolutionCache()
})
it('resolves a GHES remote whose host the user is gh-authenticated to (#8312)', async () => {
mockOriginRemote('https://github.acme-corp.com/team/orca.git')
mockHostAuthenticated()
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'github.acme-corp.com'
})
// Why: inventory configured gh hosts without targeting the untrusted remote;
// passing it as --hostname could expose an ambient enterprise token.
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(['auth', 'status'], { cwd: '/repo' })
})
it('resolves a GHES SCP-style SSH remote', async () => {
mockOriginRemote('git@github.acme-corp.com:team/orca.git')
mockHostAuthenticated()
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'github.acme-corp.com'
})
})
it('expands an SSH Host alias to the authenticated GHES HostName (#10284)', async () => {
mockOriginRemote('git@ghe-work:team/orca.git')
resolveWithSshGMock.mockResolvedValueOnce(sshConfig('github.acme-corp.com'))
mockHostAuthenticated('github.acme-corp.com')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'github.acme-corp.com'
})
expect(resolveWithSshGMock).toHaveBeenCalledWith('ghe-work')
})
it('keeps a failed GHES alias probe indeterminate and recovers on retry', async () => {
vi.useFakeTimers()
mockOriginRemote('git@ghe-work:team/orca.git')
resolveWithSshGMock
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(sshConfig('github.acme-corp.com'))
mockHostNotAuthenticated()
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeUndefined()
await vi.advanceTimersByTimeAsync(5_001)
ghExecFileAsyncMock.mockReset()
mockHostAuthenticated('github.acme-corp.com')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'github.acme-corp.com'
})
})
it('returns null for a Host alias that resolves to github.com (dotcom path owns it)', async () => {
mockOriginRemote('git@github-work:team/orca.git')
resolveWithSshGMock.mockResolvedValueOnce(sshConfig('ssh.github.com', 443))
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull()
expect(ghExecFileAsyncMock).not.toHaveBeenCalled()
})
it('expands aliases in the repository WSL runtime', async () => {
mockOriginRemote('git@github-work:team/orca.git')
commandExecFileAsyncMock.mockResolvedValueOnce({
stdout: 'hostname github.com\nport 22\n',
stderr: ''
})
await expect(
getEnterpriseGitHubRepoSlug('/repo', null, {
localGitExecOptions: { wslDistro: 'Ubuntu' }
})
).resolves.toBeNull()
expect(commandExecFileAsyncMock).toHaveBeenCalledWith('ssh', ['-G', '--', 'github-work'], {
cwd: '/repo',
timeout: 5_000,
wslDistro: 'Ubuntu'
})
expect(resolveWithSshGMock).not.toHaveBeenCalled()
})
it('uses the unique ported auth host for a hostname-only SSH remote', async () => {
mockOriginRemote('git@ghe.acme.com:team/orca.git')
mockHostAuthenticated('ghe.acme.com:8443')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'ghe.acme.com:8443'
})
})
it.each([
[['ghe.acme.com', 'ghe.acme.com:9443', 'ghe.acme.com:8443']],
[['ghe.acme.com:8443', 'ghe.acme.com:9443', 'ghe.acme.com']]
])(
'requires exact host:port authentication regardless of auth inventory order: %j',
async (authenticatedHosts) => {
mockOriginRemote('https://ghe.acme.com:8443/team/orca.git')
mockAuthenticatedHosts(authenticatedHosts)
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'ghe.acme.com:8443'
})
}
)
it.each([
'https://ghe.acme.com:8443/team/orca.git',
'https://ghe.acme.com:80/team/orca.git',
'http://ghe.acme.com:443/team/orca.git'
])('rejects portless authentication for a non-default web endpoint: %s', async (remoteUrl) => {
mockOriginRemote(remoteUrl)
mockHostAuthenticated('ghe.acme.com')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull()
})
it.each(['http://ghe.acme.com:80/team/orca.git', 'https://ghe.acme.com:443/team/orca.git'])(
'matches a protocol-default port to the portless auth host: %s',
async (remoteUrl) => {
mockOriginRemote(remoteUrl)
mockHostAuthenticated('ghe.acme.com')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'ghe.acme.com'
})
}
)
it('preserves exact authentication for an HTTP endpoint on port 443', async () => {
mockOriginRemote('http://ghe.acme.com:443/team/orca.git')
mockHostAuthenticated('ghe.acme.com:443')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'ghe.acme.com:443'
})
})
it('preserves exact authentication for an HTTPS endpoint on port 80', async () => {
mockOriginRemote('https://ghe.acme.com:80/team/orca.git')
mockHostAuthenticated('ghe.acme.com:80')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'ghe.acme.com:80'
})
})
it('probes gh in the repository WSL runtime, not the host/default distro', async () => {
mockOriginRemote('https://github.acme-corp.com/team/orca.git')
mockHostAuthenticated()
await getEnterpriseGitHubRepoSlug('/repo', null, {
localGitExecOptions: { wslDistro: 'Ubuntu' }
})
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(['auth', 'status'], {
cwd: '/repo',
wslDistro: 'Ubuntu'
})
})
it('leaves github.com to getOwnerRepo without probing gh auth', async () => {
mockOriginRemote('https://github.com/team/orca.git')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull()
expect(ghExecFileAsyncMock).not.toHaveBeenCalled()
})
it('declines a custom host the user is not gh-authenticated to (leaves it for Gitea)', async () => {
mockOriginRemote('https://gitea.example.com/team/orca.git')
mockHostNotAuthenticated()
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull()
})
it('returns null for an unparseable remote', async () => {
mockOriginRemote('not-a-remote-url')
mockHostAuthenticated()
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull()
expect(ghExecFileAsyncMock).not.toHaveBeenCalled()
})
it('returns null when the origin remote lookup fails', async () => {
gitExecFileAsyncMock.mockRejectedValue(new Error('no such remote'))
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull()
})
it('preserves an indeterminate auth inventory failure so a later probe can recover', async () => {
mockOriginRemote('git@ghe.acme.com:team/orca.git')
ghExecFileAsyncMock.mockRejectedValueOnce(
Object.assign(new Error('not installed'), { stdout: '', stderr: '' })
)
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeUndefined()
mockHostAuthenticated('ghe.acme.com:8443')
await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({
owner: 'team',
repo: 'orca',
host: 'ghe.acme.com:8443'
})
})
})
describe('isGitHubHostAuthenticated', () => {
beforeEach(() => {
ghExecFileAsyncMock.mockReset()
gitExecFileAsyncMock.mockReset()
_resetGitHubHostAuthCache()
})
it('runs gh in the SSH-local runtime (no cwd) for connection-backed repos', async () => {
mockHostAuthenticated()
await expect(
isGitHubHostAuthenticated('github.acme-corp.com', '/remote/repo', 'ssh-1')
).resolves.toBe(true)
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(['auth', 'status'], {})
})
it('caches per runtime+host so detection polling does not re-spawn gh', async () => {
mockHostAuthenticated()
await isGitHubHostAuthenticated('github.acme-corp.com', '/repo')
await isGitHubHostAuthenticated('github.acme-corp.com', '/repo')
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
})
it('coalesces concurrent probes for the same runtime and host', async () => {
let finishProbe: (() => void) | undefined
ghExecFileAsyncMock.mockImplementation(
() =>
new Promise((resolve) => {
finishProbe = () =>
resolve({
stdout:
'github.acme-corp.com\n ✓ Logged in to github.acme-corp.com account kelora (keyring)',
stderr: ''
})
})
)
const first = isGitHubHostAuthenticated('github.acme-corp.com', '/repo')
const second = isGitHubHostAuthenticated('github.acme-corp.com', '/repo')
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
finishProbe?.()
await expect(Promise.all([first, second])).resolves.toEqual([true, true])
})
it('does not share cache state across WSL distros', async () => {
mockHostAuthenticated()
await isGitHubHostAuthenticated('github.acme-corp.com', '/repo', null, { wslDistro: 'Ubuntu' })
await isGitHubHostAuthenticated('github.acme-corp.com', '/repo', null, { wslDistro: 'Debian' })
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
})
it('derives cache scope from an implicit WSL UNC repository path', async () => {
const originalPlatform = process.platform
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
ghExecFileAsyncMock.mockImplementation(async (_args: string[], options: { cwd?: string }) => ({
stdout: options.cwd?.includes('Ubuntu')
? 'github.com\n ✓ Logged in to github.com account other (keyring)'
: 'github.acme-corp.com\n ✓ Logged in to github.acme-corp.com account kelora (keyring)',
stderr: ''
}))
try {
await expect(isGitHubHostAuthenticated('github.acme-corp.com', '/native/repo')).resolves.toBe(
true
)
await expect(
isGitHubHostAuthenticated(
'github.acme-corp.com',
String.raw`\\wsl.localhost\Ubuntu\home\me\repo`
)
).resolves.toBe(false)
await expect(
isGitHubHostAuthenticated(
'github.acme-corp.com',
String.raw`\\wsl.localhost\Debian\home\me\repo`
)
).resolves.toBe(true)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(3)
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
}
})
it('does not share a cached answer between SSH connections at the same path', async () => {
ghExecFileAsyncMock
.mockResolvedValueOnce({
stdout:
'github.acme-corp.com\n ✓ Logged in to github.acme-corp.com account kelora (keyring)',
stderr: ''
})
.mockResolvedValueOnce({
stdout: 'github.com\n ✓ Logged in to github.com account kelora (keyring)',
stderr: ''
})
await expect(isGitHubHostAuthenticated('github.acme-corp.com', '/repo', 'ssh-1')).resolves.toBe(
true
)
await expect(isGitHubHostAuthenticated('github.acme-corp.com', '/repo', 'ssh-2')).resolves.toBe(
false
)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
})
it('does not reuse a cached answer after the SSH provider reconnects', async () => {
const connectionId = 'ssh-reconnected-auth-cache'
ghExecFileAsyncMock
.mockResolvedValueOnce({
stdout:
'github.acme-corp.com\n ✓ Logged in to github.acme-corp.com account kelora (keyring)',
stderr: ''
})
.mockResolvedValueOnce({
stdout: 'github.com\n ✓ Logged in to github.com account kelora (keyring)',
stderr: ''
})
registerSshGitProvider(connectionId, {} as never)
try {
await expect(
isGitHubHostAuthenticated('github.acme-corp.com', '/repo', connectionId)
).resolves.toBe(true)
await expect(
isGitHubHostAuthenticated('github.acme-corp.com', '/repo', connectionId)
).resolves.toBe(true)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
registerSshGitProvider(connectionId, {} as never)
await expect(
isGitHubHostAuthenticated('github.acme-corp.com', '/repo', connectionId)
).resolves.toBe(false)
} finally {
unregisterSshGitProvider(connectionId)
}
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
})
it('does not join an in-flight auth probe from an older SSH provider generation', async () => {
const connectionId = 'ssh-reconnected-auth-in-flight'
let finishOldProbe: (() => void) | undefined
ghExecFileAsyncMock
.mockImplementationOnce(
() =>
new Promise((resolve) => {
finishOldProbe = () =>
resolve({
stdout:
'github.acme-corp.com\n ✓ Logged in to github.acme-corp.com account kelora (keyring)',
stderr: ''
})
})
)
.mockResolvedValueOnce({
stdout: 'github.com\n ✓ Logged in to github.com account kelora (keyring)',
stderr: ''
})
registerSshGitProvider(connectionId, {} as never)
try {
const oldProbe = isGitHubHostAuthenticated('github.acme-corp.com', '/repo', connectionId)
registerSshGitProvider(connectionId, {} as never)
const newProbe = isGitHubHostAuthenticated('github.acme-corp.com', '/repo', connectionId)
await expect(newProbe).resolves.toBe(false)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
finishOldProbe?.()
await expect(oldProbe).resolves.toBe(true)
} finally {
unregisterSshGitProvider(connectionId)
}
})
// The probe answers differ by runtime: a local repo gets a cwd, a
// connection-backed repo does not. Sharing one entry attributes whichever
// resolved first to both hosts.
it.each([
['local resolves first', [null, 'ssh-1'] as const, [true, false]],
['connection resolves first', ['ssh-1', null] as const, [false, true]]
])(
'does not cross-attribute auth between a local and a connection-backed repo at the same path (%s)',
async (_label, connectionIds, expected) => {
ghExecFileAsyncMock.mockImplementation(
async (_args: string[], options: { cwd?: string }) => ({
stdout: options.cwd
? 'github.acme-corp.com\n ✓ Logged in to github.acme-corp.com account kelora (keyring)'
: 'github.com\n ✓ Logged in to github.com account kelora (keyring)',
stderr: ''
})
)
for (const [index, connectionId] of connectionIds.entries()) {
await expect(
isGitHubHostAuthenticated('github.acme-corp.com', '/repo', connectionId)
).resolves.toBe(expected[index])
}
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
}
)
it('does not target an unconfigured remote host with ambient credentials', async () => {
mockHostAuthenticated('github.acme-corp.com')
await expect(isGitHubHostAuthenticated('evil.example.test', '/repo')).resolves.toBe(false)
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(['auth', 'status'], { cwd: '/repo' })
expect(ghExecFileAsyncMock.mock.calls.flat()).not.toContain('evil.example.test')
})
it.each([
[['ghe.acme.com:8443', 'ghe.acme.com:9443']],
[['ghe.acme.com:9443', 'ghe.acme.com:8443']]
])(
'does not guess between multiple ported auth hosts for one SSH hostname: %j',
async (authenticatedHosts) => {
mockAuthenticatedHosts(authenticatedHosts)
await expect(isGitHubHostAuthenticated('ghe.acme.com', '/repo')).resolves.toBe(false)
}
)
it.each([
[
['ghe.acme.com', false],
['ghe.acme.com:8443', true]
],
[
['ghe.acme.com:8443', true],
['ghe.acme.com', false]
]
] as const)(
'keeps exact-port and ambiguous hostname auth cached independently: %j',
async (...requests) => {
mockAuthenticatedHosts(['ghe.acme.com:8443', 'ghe.acme.com:9443'])
for (const [host, expected] of requests) {
await expect(isGitHubHostAuthenticated(host, '/repo')).resolves.toBe(expected)
}
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
}
)
it('treats default web ports as the same auth host', async () => {
mockHostAuthenticated('ghe.acme.com:443')
await expect(isGitHubHostAuthenticated('ghe.acme.com', '/repo')).resolves.toBe(true)
})
it('validates a global project host by inventory without targeting it', async () => {
mockHostAuthenticated('github.acme-corp.com')
await expect(isGitHubHostAuthenticatedForGlobalCli('evil.example.test')).resolves.toBe(false)
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(['auth', 'status'], {})
expect(ghExecFileAsyncMock.mock.calls.flat()).not.toContain('evil.example.test')
})
it('treats a listed host as authenticated even when gh exits non-zero', async () => {
ghExecFileAsyncMock.mockRejectedValue(
Object.assign(new Error('exit 1'), {
stdout: '',
stderr:
'github.acme-corp.com\n ✓ Logged in to github.acme-corp.com account kelora (keyring)\n X github.com: token expired'
})
)
await expect(isGitHubHostAuthenticated('github.acme-corp.com', '/repo')).resolves.toBe(true)
})
it('does not cache a hard gh failure so a later probe can recover', async () => {
ghExecFileAsyncMock.mockRejectedValueOnce(
Object.assign(new Error('not installed'), { stdout: '', stderr: '' })
)
expect(await isGitHubHostAuthenticated('github.acme-corp.com', '/repo')).toBe(false)
mockHostAuthenticated()
expect(await isGitHubHostAuthenticated('github.acme-corp.com', '/repo')).toBe(true)
})
})