Skip to content

Commit f6ea1ff

Browse files
committed
fix(credentials): reject managed OAuth kind mismatches before resolution
1 parent 4e8919f commit f6ea1ff

3 files changed

Lines changed: 60 additions & 7 deletions

File tree

‎apps/sim/lib/internal/oracle-fusion-financials/operations.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ type OracleFusionFinancialsToolId = (typeof ORACLE_FUSION_FINANCIALS_TOOL_IDS)[n
120120

121121
const TOOL_ID_SET = new Set<string>(ORACLE_FUSION_FINANCIALS_TOOL_IDS)
122122

123+
/** Narrows a tool ID to one of the supported Oracle Fusion Financials operations. */
123124
export function isOracleFusionFinancialsToolId(
124125
value: string
125126
): value is OracleFusionFinancialsToolId {
@@ -571,6 +572,11 @@ async function getOracleFusionGlJournalBatch(rawInput: unknown, signal?: AbortSi
571572
)
572573
}
573574

575+
/**
576+
* Validates raw inputs and runs a Financials operation with optional cancellation.
577+
* Returns projected tool output, including unsuccessful Oracle action outcomes.
578+
* Input validation, provider, and cancellation errors propagate to the caller.
579+
*/
574580
export async function executeOracleFusionFinancialsOperation(
575581
toolId: OracleFusionFinancialsToolId,
576582
rawInput: unknown,

‎apps/sim/lib/oauth/token-resolution.test.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ describe('resolveCredentialAccessToken', () => {
220220
const resolveManagedPrincipal = vi.fn()
221221

222222
beforeEach(() => {
223+
mockExecuteManagedToken.mockReset()
223224
mockResolveOAuthAccountId.mockResolvedValue(null)
224225
authenticate.mockResolvedValue(INTERNAL_AUTH)
225226
resolveManagedPrincipal.mockResolvedValue(EXECUTOR_PRINCIPAL)
@@ -309,6 +310,40 @@ describe('resolveCredentialAccessToken', () => {
309310
expect(mockExecuteManagedToken).not.toHaveBeenCalled()
310311
})
311312

313+
it('rejects a managed OAuth credential for a service-account-only tool before token resolution', async () => {
314+
mockResolveOAuthAccountId.mockResolvedValueOnce({
315+
...MANAGED_RESOLVED,
316+
providerId: 'slack',
317+
})
318+
mockGetToolMetadata.mockReturnValueOnce({
319+
id: 'slack_add_bookmark',
320+
params: {},
321+
oauth: {
322+
required: true,
323+
provider: 'slack',
324+
requiredScopes: ['bookmarks:write'],
325+
credentialKind: 'service-account',
326+
},
327+
})
328+
mockExecuteManagedToken.mockResolvedValueOnce({ accessToken: 'managed-token' })
329+
330+
const result = await resolveCredentialAccessToken({
331+
requestId: 'req-1',
332+
credentialId: 'cred-1',
333+
toolId: 'slack_add_bookmark',
334+
authenticate,
335+
resolveManagedPrincipal,
336+
})
337+
338+
expect(result).toEqual({
339+
ok: false,
340+
status: 400,
341+
code: 'CREDENTIAL_TYPE_MISMATCH',
342+
error: 'The selected credential type is not supported by this tool',
343+
})
344+
expect(mockExecuteManagedToken).not.toHaveBeenCalled()
345+
})
346+
312347
it('rejects tools whose scope policy is empty', async () => {
313348
mockResolveOAuthAccountId.mockResolvedValue(MANAGED_RESOLVED)
314349
mockGetToolMetadata.mockReturnValue({

‎apps/sim/lib/oauth/token-resolution.ts‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
} from '@/lib/oauth/utils'
3636
import { captureServerEvent } from '@/lib/posthog/server'
3737
import { getToolMetadata } from '@/tools/metadata'
38+
import type { OAuthConfig } from '@/tools/types'
3839
import { extractZohoDeskBaseFromScope } from '@/tools/zoho_desk/host-allowlist'
3940

4041
const logger = createLogger('OAuthTokenResolution')
@@ -233,21 +234,29 @@ export async function completeOAuthCredentialToken(params: {
233234
}
234235
}
235236

236-
function validateToolCredentialBinding(
237-
toolId: string | undefined,
238-
providerId: string | null | undefined,
239-
credentialKind: 'oauth' | 'service-account'
237+
function validateToolCredentialKind(
238+
requiredKind: OAuthConfig['credentialKind'],
239+
actualKind: NonNullable<OAuthConfig['credentialKind']>
240240
): Extract<ResolveCredentialTokenResult, { ok: false }> | undefined {
241-
if (!toolId) return
242-
const policy = getToolMetadata(toolId)?.oauth
243-
if (policy?.credentialKind && policy.credentialKind !== credentialKind) {
241+
if (requiredKind && requiredKind !== actualKind) {
244242
return {
245243
ok: false,
246244
status: 400,
247245
code: 'CREDENTIAL_TYPE_MISMATCH',
248246
error: 'The selected credential type is not supported by this tool',
249247
}
250248
}
249+
}
250+
251+
function validateToolCredentialBinding(
252+
toolId: string | undefined,
253+
providerId: string | null | undefined,
254+
credentialKind: NonNullable<OAuthConfig['credentialKind']>
255+
): Extract<ResolveCredentialTokenResult, { ok: false }> | undefined {
256+
if (!toolId) return
257+
const policy = getToolMetadata(toolId)?.oauth
258+
const kindError = validateToolCredentialKind(policy?.credentialKind, credentialKind)
259+
if (kindError) return kindError
251260
const service = policy?.required
252261
? (getServiceConfigByServiceId(policy.provider) ??
253262
getServiceConfigByProviderId(policy.provider))
@@ -509,6 +518,9 @@ export async function resolveCredentialAccessToken(
509518
error: 'This tool is not configured to use managed credentials',
510519
}
511520
}
521+
const kindError = validateToolCredentialKind(toolMetadata.oauth.credentialKind, 'oauth')
522+
if (kindError) return kindError
523+
512524
const requiredScopes =
513525
toolMetadata.oauth.requiredScopes ?? getCanonicalScopesForProvider(toolMetadata.oauth.provider)
514526
if (requiredScopes.length === 0 && toolMetadata.oauth.requiredScopes === undefined) {

0 commit comments

Comments
 (0)