Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions apps/sim/app/api/auth/sso/providers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

const logger = createLogger('SSOProvidersRoute')

/** Secrets shorter than this reveal too large a fraction of themselves in 4 characters. */
const MIN_LENGTH_FOR_HINT = 16

/**
* Last four characters of a stored client secret, so an admin can tell *which*
* secret is saved rather than only that one exists. Four characters of a
* high-entropy secret is not a meaningful disclosure to an owner or admin, who
* can rotate it anyway — but short secrets are left unhinted, where the same four
* characters would be a large share of the value.
*/
function buildClientSecretHint(clientSecret: unknown): string | null {
if (typeof clientSecret !== 'string' || clientSecret.length < MIN_LENGTH_FOR_HINT) return null
return clientSecret.slice(-4)
}

export const GET = withRouteHandler(async (request: NextRequest) => {
try {
const session = await getSession()
Expand Down Expand Up @@ -69,7 +84,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
if (oidcConfig) {
try {
const parsed = JSON.parse(oidcConfig)
const hint = buildClientSecretHint(parsed.clientSecret)
parsed.clientSecret = REDACTED_MARKER
if (hint) parsed.clientSecretHint = hint
oidcConfig = JSON.stringify(parsed)
} catch {
oidcConfig = null
Expand Down
91 changes: 91 additions & 0 deletions apps/sim/app/api/memory/[id]/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* @vitest-environment node
*/

import { memory } from '@sim/db/schema'
import {
createMockRequest,
hybridAuthMockFns,
queueTableRows,
resetDbChainMock,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthType } from '@/lib/auth/hybrid'
import {
PRIVATE_TOOL_METADATA_REQUEST_HEADER,
PRIVATE_TOOL_METADATA_RESPONSE_HEADER,
RESOLVED_SECRET_PROVENANCE_FIELD,
RESOLVED_SECRET_PROVENANCE_METADATA_V1,
} from '@/lib/execution/private-tool-metadata'

const { mockCheckWorkspaceAccess } = vi.hoisted(() => ({
mockCheckWorkspaceAccess: vi.fn(),
}))

vi.mock('@/lib/workspaces/permissions/utils', () => ({
checkWorkspaceAccess: mockCheckWorkspaceAccess,
}))

import { GET } from '@/app/api/memory/[id]/route'

const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111'
const CONTEXT = { params: Promise.resolve({ id: 'missing-conversation' }) }

describe('GET /api/memory/[id]', () => {
beforeEach(() => {
vi.clearAllMocks()
resetDbChainMock()
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
authType: AuthType.INTERNAL_JWT,
})
mockCheckWorkspaceAccess.mockResolvedValue({ exists: true, hasAccess: true })
queueTableRows(memory, [])
})

it('returns verified exact-empty metadata when a tool lookup has no matching memory', async () => {
const response = await GET(
createMockRequest(
'GET',
undefined,
{
[PRIVATE_TOOL_METADATA_REQUEST_HEADER]: RESOLVED_SECRET_PROVENANCE_METADATA_V1,
},
`http://localhost:3000/api/memory/missing-conversation?workspaceId=${WORKSPACE_ID}`
),
CONTEXT
)

expect(response.status).toBe(200)
expect(response.headers.get(PRIVATE_TOOL_METADATA_RESPONSE_HEADER)).toBe(
RESOLVED_SECRET_PROVENANCE_METADATA_V1
)
expect(await response.json()).toEqual({
success: true,
data: null,
[RESOLVED_SECRET_PROVENANCE_FIELD]: {
version: 1,
complete: true,
entries: [],
scope: { userId: 'user-1', workspaceId: WORKSPACE_ID },
},
})
})

it('preserves the existing headerless empty response for ordinary API callers', async () => {
const response = await GET(
createMockRequest(
'GET',
undefined,
{},
`http://localhost:3000/api/memory/missing-conversation?workspaceId=${WORKSPACE_ID}`
),
CONTEXT
)

expect(response.status).toBe(200)
expect(response.headers.get(PRIVATE_TOOL_METADATA_RESPONSE_HEADER)).toBeNull()
expect(await response.json()).toEqual({ success: true, data: null })
})
})
9 changes: 8 additions & 1 deletion apps/sim/app/api/memory/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,14 @@ export const GET = withRouteHandler(async (request: NextRequest, context: Memory
.limit(1)

if (memories.length === 0) {
return NextResponse.json({ success: true, data: null }, { status: 200 })
return createMemoryResponse({
request,
authType: accessCheck.authType,
userId: accessCheck.userId,
workspaceId: validatedWorkspaceId,
body: { success: true, data: null },
memories: [],
})
}

const mem = memories[0]
Expand Down
223 changes: 223 additions & 0 deletions apps/sim/app/api/table/row-secret-provenance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/**
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import { describe, expect, it } from 'vitest'
import { AuthType } from '@/lib/auth/hybrid'
import {
PRIVATE_SECRET_PROVENANCE_BUNDLE_V1,
PRIVATE_SECRET_PROVENANCE_FIELD,
PRIVATE_SECRET_PROVENANCE_HEADER,
} from '@/lib/execution/private-tool-metadata'
import { rowDataNameToId } from '@/lib/table/column-keys'
import { tableRowSecretProvenanceSelectionKey } from '@/lib/table/secret-provenance-selection'
import type { RowData } from '@/lib/table/types'
import {
createTableWriteProvenanceTargets,
resolveTableWriteSecretProvenance,
} from '@/app/api/table/row-secret-provenance'

const USER_ID = 'user-1'
const WORKSPACE_ID = 'ws-1'

/** Mirrors the internal-JWT wire translator: names → ids, unknown names dropped. */
const ID_BY_NAME = new Map([
['email', 'col_email'],
['company', 'col_company'],
])

const translateNames = (data: RowData): RowData => rowDataNameToId(data, ID_BY_NAME)
const translateIdentity = (data: RowData): RowData => data

function traceProvenance() {
return {
version: 1,
complete: true,
entries: [],
scope: { userId: USER_ID, workspaceId: WORKSPACE_ID },
}
}

function bundleRequest(selectionKeys: string[]) {
const payload = {
[PRIVATE_SECRET_PROVENANCE_FIELD]: {
version: 1,
complete: true,
selections: selectionKeys.map((key) => ({ key, provenance: traceProvenance() })),
},
}
const request = createMockRequest('POST', payload, {
[PRIVATE_SECRET_PROVENANCE_HEADER]: PRIVATE_SECRET_PROVENANCE_BUNDLE_V1,
})
return { request, payload }
}

describe('createTableWriteProvenanceTargets', () => {
it('maps column names to their storage ids', () => {
const targets = createTableWriteProvenanceTargets([{ email: 'a@b.c' }], translateNames)

expect(targets).toEqual([
{
selectionKey: tableRowSecretProvenanceSelectionKey(0, 'email'),
rowKey: '0',
columnId: 'col_email',
},
])
})

it('returns a null column id for a column the wire translator drops', () => {
const targets = createTableWriteProvenanceTargets(
[{ email: 'a@b.c', notAColumn: 'x' }],
translateNames
)

expect(targets).toHaveLength(2)
expect(targets[0].columnId).toBe('col_email')
expect(targets[1]).toEqual({
selectionKey: tableRowSecretProvenanceSelectionKey(0, 'notAColumn'),
rowKey: '0',
columnId: null,
})
})

it('keeps one target per submitted column so bundle selections stay paired', () => {
const targets = createTableWriteProvenanceTargets(
[{ notAColumn: 'x', alsoNotAColumn: 'y' }],
translateNames
)

expect(targets.map((target) => target.columnId)).toEqual([null, null])
})

it('passes column ids through for identity (session) translation', () => {
const targets = createTableWriteProvenanceTargets([{ col_email: 'a@b.c' }], translateIdentity)

expect(targets[0].columnId).toBe('col_email')
})

it('keys targets by row index across multiple rows', () => {
const targets = createTableWriteProvenanceTargets(
[{ email: 'a@b.c' }, { company: 'Acme' }],
translateNames
)

expect(targets.map((target) => target.rowKey)).toEqual(['0', '1'])
expect(targets[1].selectionKey).toBe(tableRowSecretProvenanceSelectionKey(1, 'company'))
})
})

describe('resolveTableWriteSecretProvenance', () => {
it('records no provenance for a dropped column on an unsupported session write', () => {
const rows = [{ email: 'a@b.c', notAColumn: 'x' }]
const result = resolveTableWriteSecretProvenance({
request: createMockRequest('POST', { rows }),
payload: { rows },
authType: AuthType.SESSION,
userId: USER_ID,
workspaceId: WORKSPACE_ID,
targets: createTableWriteProvenanceTargets(rows, translateNames),
rowKeys: ['0'],
})

expect(result.success).toBe(true)
if (!result.success) return
expect(Object.keys(result.provenanceByRowKey?.['0'].columns ?? {})).toEqual(['col_email'])
})

it('accepts a complete bundle that covers a dropped column', () => {
const rows = [{ email: 'a@b.c', notAColumn: 'x' }]
const { request, payload } = bundleRequest([
tableRowSecretProvenanceSelectionKey(0, 'email'),
tableRowSecretProvenanceSelectionKey(0, 'notAColumn'),
])

const result = resolveTableWriteSecretProvenance({
request,
payload,
authType: AuthType.INTERNAL_JWT,
userId: USER_ID,
workspaceId: WORKSPACE_ID,
targets: createTableWriteProvenanceTargets(rows, translateNames),
rowKeys: ['0'],
})

expect(result.success).toBe(true)
if (!result.success) return
expect(Object.keys(result.provenanceByRowKey?.['0'].columns ?? {})).toEqual(['col_email'])
})

it('stores provenance for a fully translatable bundle', () => {
const rows = [{ email: 'a@b.c', company: 'Acme' }]
const { request, payload } = bundleRequest([
tableRowSecretProvenanceSelectionKey(0, 'email'),
tableRowSecretProvenanceSelectionKey(0, 'company'),
])

const result = resolveTableWriteSecretProvenance({
request,
payload,
authType: AuthType.INTERNAL_JWT,
userId: USER_ID,
workspaceId: WORKSPACE_ID,
targets: createTableWriteProvenanceTargets(rows, translateNames),
rowKeys: ['0'],
})

expect(result.success).toBe(true)
if (!result.success) return
expect(Object.keys(result.provenanceByRowKey?.['0'].columns ?? {}).sort()).toEqual([
'col_company',
'col_email',
])
})

it('rejects a bundle whose selection matches no submitted column', () => {
const rows = [{ email: 'a@b.c' }]
const { request, payload } = bundleRequest([tableRowSecretProvenanceSelectionKey(0, 'company')])

const result = resolveTableWriteSecretProvenance({
request,
payload,
authType: AuthType.INTERNAL_JWT,
userId: USER_ID,
workspaceId: WORKSPACE_ID,
targets: createTableWriteProvenanceTargets(rows, translateNames),
rowKeys: ['0'],
})

expect(result.success).toBe(false)
})

it('rejects a bundle whose selection scope does not match the caller', () => {
const rows = [{ email: 'a@b.c' }]
const payload = {
[PRIVATE_SECRET_PROVENANCE_FIELD]: {
version: 1,
complete: true,
selections: [
{
key: tableRowSecretProvenanceSelectionKey(0, 'email'),
provenance: {
...traceProvenance(),
scope: { userId: 'someone-else', workspaceId: WORKSPACE_ID },
},
},
],
},
}

const result = resolveTableWriteSecretProvenance({
request: createMockRequest('POST', payload, {
[PRIVATE_SECRET_PROVENANCE_HEADER]: PRIVATE_SECRET_PROVENANCE_BUNDLE_V1,
}),
payload,
authType: AuthType.INTERNAL_JWT,
userId: USER_ID,
workspaceId: WORKSPACE_ID,
targets: createTableWriteProvenanceTargets(rows, translateNames),
rowKeys: ['0'],
})

expect(result.success).toBe(false)
})
})
Loading
Loading