Skip to content

Commit ff9ddac

Browse files
committed
fix(search): clear unverifiable placeholder permissions
1 parent e07224b commit ff9ddac

3 files changed

Lines changed: 113 additions & 9 deletions

File tree

‎apps/sim/connectors/google-workspace/company-work.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface GoogleCompanyUserWork {
77
cursor?: string
88
}
99

10+
/** Seeds per-user provider cursors and adopts legacy crawls without losing the active continuation. */
1011
export interface GoogleCompanyCursorAdapter {
1112
seed: (user: GoogleCompanyUserWork['user']) => string
1213
/** Retains the exact active page when adopting the former single-user-at-a-time cursor. */

‎apps/sim/lib/knowledge/__integration__/listing-continuation.integration.ts‎

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ import {
9494
} from '@/lib/knowledge/connectors/member-sync-engine'
9595
import { runConnectorContentPass } from '@/lib/knowledge/connectors/sync-content-pass'
9696
import { executeSync } from '@/lib/knowledge/connectors/sync-engine'
97-
import { SOURCE_CONTENT_ERROR } from '@/lib/knowledge/connectors/sync-limits'
97+
import {
98+
SOURCE_CONTENT_ERROR,
99+
SOURCE_PERMISSION_ERROR,
100+
} from '@/lib/knowledge/connectors/sync-limits'
98101
import { createContentSyncLease, createMemberSyncLease } from '@/lib/knowledge/connectors/sync-lock'
99102
import { addDocument, persistDocumentAcls } from '@/lib/knowledge/connectors/sync-persistence'
100103
import * as documentService from '@/lib/knowledge/documents/service'
@@ -240,11 +243,11 @@ describe('durable source and member cycles in PostgreSQL', () => {
240243
forceRehydrate: false,
241244
deadlineAt: Date.now() + 60_000,
242245
onPage: async (verified) => {
243-
await persistDocumentAcls(
246+
const write = await persistDocumentAcls(
244247
connectorId,
245248
new Map(verified.map((item) => [item.externalId, item.acl!]))
246249
)
247-
return { permissionsIncomplete: false }
250+
return { permissionsIncomplete: write.rejected > 0 }
248251
},
249252
})
250253
expect(getDocument.mock.calls.map(([id]) => id).sort()).toEqual([
@@ -279,6 +282,101 @@ describe('durable source and member cycles in PostgreSQL', () => {
279282
})
280283
})
281284

285+
it.each([
286+
{ description: 'a matching-hash body placeholder', storageKey: null, rejectAcl: false },
287+
{ description: 'an unusable source ACL', storageKey: 'kb/verified-body.txt', rejectAcl: true },
288+
])('clears stale permission evidence for $description', async ({ storageKey, rejectAcl }) => {
289+
const connectorId = generateId()
290+
const runId = generateId()
291+
const externalId = 'permission-evidence'
292+
const oldVerifiedAt = new Date(Date.now() - 25 * 60 * 60 * 1000)
293+
const ownerAcl = [`u:${ids.aliceId}@fixture.test`]
294+
await db.insert(knowledgeConnector).values({
295+
id: connectorId,
296+
knowledgeBaseId: ids.knowledgeBaseId,
297+
connectorType: 'google_drive',
298+
status: 'syncing',
299+
syncLockToken: runId,
300+
accessMode: 'admin',
301+
sourceConfig: {},
302+
})
303+
await db.insert(document).values({
304+
id: generateId(),
305+
knowledgeBaseId: ids.knowledgeBaseId,
306+
connectorId,
307+
externalId,
308+
filename: externalId,
309+
fileUrl: '',
310+
storageKey,
311+
fileSize: storageKey === null ? 0 : 10,
312+
mimeType: 'text/plain',
313+
contentHash: `hash-${externalId}`,
314+
processingStatus: storageKey === null ? 'failed' : 'completed',
315+
processingError: storageKey === null ? 'Unsupported source file type' : null,
316+
acl: ownerAcl,
317+
aclRequirements: [ownerAcl, ['d:fixture.test']],
318+
aclVerifiedAt: oldVerifiedAt,
319+
sourceSeenAt: oldVerifiedAt,
320+
})
321+
const listDocuments = vi.fn<ConnectorConfig['listDocuments']>(async () => ({
322+
documents: [
323+
{
324+
...sourceDoc(externalId),
325+
content: '',
326+
contentDeferred: true,
327+
acl: rejectAcl ? ['invalid-principal'] : ownerAcl,
328+
},
329+
],
330+
hasMore: false,
331+
permissionsOnly: true,
332+
reconciliationSafe: false,
333+
}))
334+
const getDocument = vi.fn(async () => sourceDoc(externalId))
335+
const [connector] = await db
336+
.select()
337+
.from(knowledgeConnector)
338+
.where(eq(knowledgeConnector.id, connectorId))
339+
const pass = await runConnectorContentPass({
340+
connectorId,
341+
connector,
342+
connectorConfig: { ...CONNECTOR_REGISTRY.google_drive, listDocuments },
343+
sourceConfig: {},
344+
syncContext: {},
345+
kbOwner: { userId: ids.aliceId, workspaceId: ids.workspaceId },
346+
billingAttribution: billing,
347+
result: result(),
348+
lease: createContentSyncLease(connectorId, runId),
349+
leaseKind: 'content',
350+
runId,
351+
fingerprint: listingFingerprint({ source: 'permission-evidence' }),
352+
documentAccess: 'admin',
353+
getAccessToken: async () => 'fixture',
354+
hydration: { getDocument },
355+
forceRehydrate: false,
356+
deadlineAt: Date.now() + 60_000,
357+
onPage: async (verified) => {
358+
const write = await persistDocumentAcls(
359+
connectorId,
360+
new Map(verified.map((item) => [item.externalId, item.acl!]))
361+
)
362+
return { permissionsIncomplete: write.rejected > 0 }
363+
},
364+
})
365+
const [stored] = await db.select().from(document).where(eq(document.connectorId, connectorId))
366+
expect(stored).toMatchObject({
367+
acl: [],
368+
aclRequirements: [],
369+
aclVerifiedAt: null,
370+
sourceSeenAt: oldVerifiedAt,
371+
contentHash: `hash-${externalId}`,
372+
storageKey,
373+
deletedAt: null,
374+
})
375+
expect(getDocument).not.toHaveBeenCalled()
376+
expect(pass.checkpoint.permissionFailures).toBe(rejectAcl)
377+
if (rejectAcl) expect(pass.holdNotice).toBe(SOURCE_PERMISSION_ERROR)
378+
})
379+
282380
it('preserves a permission-repaired document through the remaining content crawl and EOF reconciliation', async () => {
283381
const connectorId = generateId()
284382
const runId = generateId()
@@ -378,11 +476,11 @@ describe('durable source and member cycles in PostgreSQL', () => {
378476
forceRehydrate: false,
379477
deadlineAt: Date.now() + 60_000,
380478
onPage: async (verified) => {
381-
await persistDocumentAcls(
479+
const write = await persistDocumentAcls(
382480
connectorId,
383481
new Map(verified.map((item) => [item.externalId, item.acl!]))
384482
)
385-
return { permissionsIncomplete: false }
483+
return { permissionsIncomplete: write.rejected > 0 }
386484
},
387485
})
388486
return { pass, stats }
@@ -520,11 +618,11 @@ describe('durable source and member cycles in PostgreSQL', () => {
520618
forceRehydrate: false,
521619
deadlineAt: Date.now() + 60_000,
522620
onPage: async (verified) => {
523-
await persistDocumentAcls(
621+
const write = await persistDocumentAcls(
524622
connectorId,
525623
new Map(verified.map((item) => [item.externalId, item.acl!]))
526624
)
527-
return { permissionsIncomplete: false }
625+
return { permissionsIncomplete: write.rejected > 0 }
528626
},
529627
})
530628
expect(pass.complete).toBe(true)

‎apps/sim/lib/knowledge/connectors/sync-content-pass.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,14 @@ export async function runConnectorContentPass(input: ContentPassInput) {
163163
if (page.permissionsOnly) {
164164
const changed = documents.filter((item) => {
165165
const prior = corpus.priorByExternalId.get(item.externalId)
166-
return prior && (!prior.contentHash || prior.contentHash !== item.contentHash)
166+
return (
167+
prior &&
168+
(!prior.contentHash ||
169+
prior.contentHash !== item.contentHash ||
170+
prior.storageKey === null)
171+
)
167172
})
168-
/** Revoke stale-body grants while retaining the content crawl's observation for EOF reconciliation. */
173+
/** Revoke grants without matching stored content, retaining the content crawl's observation for EOF reconciliation. */
169174
if (changed.length)
170175
await withLease(async (tx) => {
171176
for (let offset = 0; offset < changed.length; offset += 500) {

0 commit comments

Comments
 (0)