fix(sidebar): reject stale folder owners

This commit is contained in:
Brennan Benson
2026-08-10 13:13:38 -07:00
parent cd7cf2fd17
commit 2e0c0c0332
6 changed files with 70 additions and 18 deletions
+25
View File
@@ -302,6 +302,31 @@ describe('filesystem-auth path containment', () => {
}
})
it('rejects a foreign-stamped folder whose group exists only locally', async () => {
const tempRoot = await mkdtemp(join(tmpdir(), 'orca-auth-stale-folder-owner-'))
try {
const localPath = join(tempRoot, 'local')
const foreignPath = join(tempRoot, 'foreign')
await mkdir(localPath)
await mkdir(foreignPath)
const store = makeStore([], {
projectGroups: [makeProjectGroup({ id: 'same-id', parentPath: localPath })],
folderWorkspaces: [
makeFolderWorkspace({
id: 'foreign-folder',
projectGroupId: 'same-id',
folderPath: foreignPath,
connectionId: 'missing'
})
]
})
await expect(resolveAuthorizedPath(foreignPath, store)).rejects.toThrow('Access denied')
} finally {
await rm(tempRoot, { recursive: true, force: true })
}
})
it('keeps large multi-owner folder authorization linear in catalog size', () => {
const groupCount = 2_048
const repoCount = 2_048
@@ -35,6 +35,13 @@ describe('project-group sidebar identity', () => {
expect(getProjectGroupSidebarIdentity(local)).not.toBe(getProjectGroupSidebarIdentity(runtime))
})
it('does not fall back to a foreign group for an explicit stale owner', () => {
const local = group('same-id', 'local')
const index = buildProjectGroupSidebarIndex([local])
expect(findProjectGroupForSidebarOwner(index, 'same-id', 'runtime:missing')).toBeUndefined()
})
it('disables focused-host mutations for mixed-owner and mismatched lists', () => {
const localA = group('a', 'local')
const localB = group('b', 'local')
@@ -103,10 +103,7 @@ export function findProjectGroupForSidebarOwner(
return undefined
}
if (ownerHostId) {
const exact = index.byIdentity.get(JSON.stringify([ownerHostId, groupId]))
if (exact) {
return exact
}
return index.byIdentity.get(JSON.stringify([ownerHostId, groupId]))
}
return index.byUnambiguousId.get(groupId)
}
+6 -8
View File
@@ -7,8 +7,7 @@ import {
import {
buildProjectGroupOwnerIndex,
getFolderWorkspaceProjectGroupOwnerHostId,
resolveFolderWorkspaceProjectGroup,
resolveProjectGroupOwner
resolveFolderWorkspaceProjectGroup
} from './project-groups'
import type { FolderWorkspace, ProjectGroup } from './types'
import { isTuiAgent } from './tui-agent-config'
@@ -107,12 +106,11 @@ export function normalizeFolderWorkspaces(
continue
}
const executionHostId = normalizeExecutionHostId(raw.executionHostId)
const group =
resolveFolderWorkspaceProjectGroup(projectGroupIndex, {
projectGroupId: raw.projectGroupId,
connectionId: raw.connectionId,
executionHostId
}) ?? resolveProjectGroupOwner(projectGroupIndex, raw.projectGroupId)
const group = resolveFolderWorkspaceProjectGroup(projectGroupIndex, {
projectGroupId: raw.projectGroupId,
connectionId: raw.connectionId,
executionHostId
})
if (!group?.parentPath) {
continue
}
+23
View File
@@ -13,6 +13,7 @@ import {
resolveFolderWorkspaceProjectGroup,
resolveProjectGroupOwner
} from './project-groups'
import { normalizeFolderWorkspaces } from './folder-workspaces'
import type { ProjectGroup, Repo } from './types'
function repo(overrides: Partial<Repo>): Repo {
@@ -162,6 +163,28 @@ describe('project-groups', () => {
expect(resolveFolderWorkspaceProjectGroup(index, { projectGroupId: 'same-id' })).toBeNull()
})
it('rejects folder workspaces stamped for missing foreign owners', () => {
const local = projectGroup({ id: 'same-id', name: 'Local', parentPath: '/local' })
expect(
normalizeFolderWorkspaces(
[
{
id: 'runtime-folder',
projectGroupId: 'same-id',
executionHostId: 'runtime:missing'
},
{
id: 'ssh-folder',
projectGroupId: 'same-id',
connectionId: 'missing'
}
],
[local]
)
).toEqual([])
})
it('collects descendants only from the selected owner hierarchy', () => {
const localRoot = projectGroup({ id: 'root', name: 'Local root' })
const localChild = projectGroup({ id: 'child', parentGroupId: 'root' })
+8 -6
View File
@@ -17,11 +17,10 @@ export type ProjectGroupOwnerIndex = {
export function getProjectGroupOwnerHostId(
group: Pick<ProjectGroup, 'connectionId' | 'executionHostId'>
): ExecutionHostId {
const executionHostId = normalizeExecutionHostId(group.executionHostId)
if (executionHostId) {
return executionHostId
}
return group.connectionId ? toSshExecutionHostId(group.connectionId) : LOCAL_EXECUTION_HOST_ID
return (
normalizeExecutionHostId(group.executionHostId) ??
(group.connectionId ? toSshExecutionHostId(group.connectionId) : LOCAL_EXECUTION_HOST_ID)
)
}
export function getProjectGroupOwnerIdentity(
@@ -99,7 +98,10 @@ export function resolveFolderWorkspaceProjectGroup(
? toSshExecutionHostId(workspace.connectionId)
: LOCAL_EXECUTION_HOST_ID
const group = resolveProjectGroupMembership(index, workspace.projectGroupId, ownerHostId)
return group ?? resolveProjectGroupOwner(index, workspace.projectGroupId)
// Why: null predates owner stamps; a named SSH owner is authoritative.
return workspace.connectionId === null
? (group ?? resolveProjectGroupOwner(index, workspace.projectGroupId))
: group
}
return resolveProjectGroupOwner(index, workspace.projectGroupId)
}