Skip to content

Commit 590c9a3

Browse files
committed
fix(connectors): fail closed on incomplete SharePoint traversal
1 parent cc5a39a commit 590c9a3

2 files changed

Lines changed: 90 additions & 7 deletions

File tree

apps/sim/connectors/sharepoint/sharepoint.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,29 @@ function rootChildren(driveId: string, items: unknown[]) {
105105
}
106106
}
107107

108+
/** Builds a Graph pagination chain with an optional continuation beyond the final allowed page. */
109+
function paginatedRoutes(
110+
initialUrl: string,
111+
routePrefix: string,
112+
pageCount: number,
113+
continueAfterLast: boolean
114+
): Record<string, GraphRoute> {
115+
const routes: Record<string, GraphRoute> = {}
116+
117+
for (let page = 0; page < pageCount; page++) {
118+
const url = page === 0 ? initialUrl : `${GRAPH}/${routePrefix}/${page}`
119+
const hasNextPage = page < pageCount - 1 || continueAfterLast
120+
routes[url] = {
121+
body: {
122+
value: [],
123+
...(hasNextPage ? { '@odata.nextLink': `${GRAPH}/${routePrefix}/${page + 1}` } : {}),
124+
},
125+
}
126+
}
127+
128+
return routes
129+
}
130+
108131
function resolve(folderPath?: string) {
109132
return resolveFolderTarget('token', SITE_ID, SITE_URL, 'Contoso', folderPath)
110133
}
@@ -322,6 +345,51 @@ describe('resolveFolderTarget', () => {
322345
)
323346
})
324347

348+
it('surfaces a terminal document-library listing failure', async () => {
349+
mockGraph({
350+
...defaultDriveRoute,
351+
[`${GRAPH}/sites/${SITE_ID}/drives?$select=id,name,webUrl`]: {
352+
status: 503,
353+
body: { error: { message: 'Service unavailable' } },
354+
},
355+
})
356+
357+
await expect(resolve('Missing')).rejects.toThrow(
358+
/Failed to list SharePoint document libraries: 503/
359+
)
360+
})
361+
362+
it('rejects a document-library listing that continues beyond its safety limit', async () => {
363+
const initialUrl = `${GRAPH}/sites/${SITE_ID}/drives?$select=id,name,webUrl`
364+
mockGraph({
365+
...defaultDriveRoute,
366+
...paginatedRoutes(initialUrl, 'drive-pages', 20, true),
367+
})
368+
369+
await expect(resolve('Missing')).rejects.toThrow(
370+
/document-library listing exceeded the 20-page safety limit/
371+
)
372+
})
373+
374+
it('surfaces a 404 encountered while traversing a folder collection', async () => {
375+
mockGraph({ ...defaultDriveRoute, ...sitesDrivesRoute })
376+
377+
await expect(resolve('Missing')).rejects.toThrow(/Failed to list folder contents: 404/)
378+
})
379+
380+
it('rejects a folder listing that continues beyond its safety limit', async () => {
381+
const initialUrl = `${GRAPH}/drives/${DEFAULT_DRIVE_ID}/root/children?$top=200&$select=id,name,folder`
382+
mockGraph({
383+
...defaultDriveRoute,
384+
...sitesDrivesRoute,
385+
...paginatedRoutes(initialUrl, 'folder-pages', 50, true),
386+
})
387+
388+
await expect(resolve('Missing')).rejects.toThrow(
389+
/folder listing exceeded the 50-page safety limit/
390+
)
391+
})
392+
325393
it('accepts an address-bar folder URL carrying the path in the id parameter', async () => {
326394
mockGraph({
327395
...defaultDriveRoute,

apps/sim/connectors/sharepoint/sharepoint.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -425,9 +425,9 @@ async function listChildFolders(
425425

426426
for (let page = 0; page < MAX_CHILD_PAGES_PER_SEGMENT; page++) {
427427
const response = await graphGet(url, accessToken, retryOptions)
428-
if (response.status === 404) break
429428
if (!response.ok) {
430-
throw new Error(`Failed to list folder contents: ${response.status}`)
429+
const errorText = await readBoundedHttpErrorBody(response)
430+
throw new Error(`Failed to list folder contents: ${response.status}${errorText}`)
431431
}
432432

433433
const rawData: unknown = await response.json()
@@ -455,11 +455,16 @@ async function listChildFolders(
455455
rawData['@odata.nextLink'] === undefined
456456
? undefined
457457
: assertMicrosoftGraphNextLink(rawData['@odata.nextLink'])
458-
if (!nextLink) break
458+
if (!nextLink) return folders
459+
if (page === MAX_CHILD_PAGES_PER_SEGMENT - 1) {
460+
throw new Error(
461+
`SharePoint folder listing exceeded the ${MAX_CHILD_PAGES_PER_SEGMENT}-page safety limit`
462+
)
463+
}
459464
url = nextLink
460465
}
461466

462-
return folders
467+
throw new Error('SharePoint folder listing ended unexpectedly')
463468
}
464469

465470
/**
@@ -668,15 +673,25 @@ async function listSiteDrives(
668673

669674
for (let page = 0; page < MAX_DRIVE_PAGES; page++) {
670675
const response = await graphGet(url, accessToken, retryOptions)
671-
if (!response.ok) break
676+
if (!response.ok) {
677+
const errorText = await readBoundedHttpErrorBody(response)
678+
throw new Error(
679+
`Failed to list SharePoint document libraries: ${response.status}${errorText}`
680+
)
681+
}
672682
const data = parseDriveListResponse(await response.json())
673683
drives.push(...data.value)
674684
const nextLink = data['@odata.nextLink']
675-
if (!nextLink) break
685+
if (!nextLink) return drives
686+
if (page === MAX_DRIVE_PAGES - 1) {
687+
throw new Error(
688+
`SharePoint document-library listing exceeded the ${MAX_DRIVE_PAGES}-page safety limit`
689+
)
690+
}
676691
url = nextLink
677692
}
678693

679-
return drives
694+
throw new Error('SharePoint document-library listing ended unexpectedly')
680695
}
681696

682697
/**

0 commit comments

Comments
 (0)