Skip to content

Commit 8fd3532

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(oracle-fusion): harden SCM integration boundaries
1 parent 472450d commit 8fd3532

17 files changed

Lines changed: 417 additions & 45 deletions

File tree

apps/sim/app/(landing)/integrations/(shell)/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const TOTAL_TOOL_COUNT = allIntegrations.reduce((sum, i) => sum + i.operationCou
3232
const CATALOG_FAQS: FAQItem[] = [
3333
{
3434
question: 'How do integrations work in Sim?',
35-
answer: `Each integration is a block you drag onto Sim's workflow builder. Together, Sim's ${INTEGRATION_COUNT} integrations expose ${TOTAL_TOOL_COUNT}+ tools that AI agents can call. ${OAUTH_COUNT} connect with one-click OAuth, ${SERVICE_ACCOUNT_COUNT} use reusable service-account credentials, and the rest use an API key or no authentication at all. Wire blocks together, add an AI agent block for reasoning, and run.`,
35+
answer: `Each integration is a block you drag onto Sim's workflow builder. Together, Sim's ${INTEGRATION_COUNT} integrations expose ${TOTAL_TOOL_COUNT}+ tools that AI agents can call. ${OAUTH_COUNT} connect with one-click OAuth, ${SERVICE_ACCOUNT_COUNT} use reusable service-account credentials, and the rest are configured directly in the block with an API key, other connection details, or no credentials as applicable. Wire blocks together, add an AI agent block for reasoning, and run.`,
3636
},
3737
{
3838
question: 'Are Sim integrations free to use?',

apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.test.tsx

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { act } from 'react'
55
import { createRoot, type Root } from 'react-dom/client'
66
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
77

8-
const { mockCaptureEvent, modeState } = vi.hoisted(() => ({
8+
const { credentialsState, mockCaptureEvent, modeState, oauthServicesState } = vi.hoisted(() => ({
9+
credentialsState: { data: [] as unknown[] },
910
mockCaptureEvent: vi.fn(),
1011
/** The URL `mode` param as the nuqs mock serves it; `set` is the live setter once mounted. */
1112
modeState: { initial: 'build', set: (_next: string) => {} },
13+
oauthServicesState: { data: [] as unknown[] },
1214
}))
1315

1416
vi.mock('nuqs', async () => {
@@ -30,10 +32,10 @@ vi.mock('@/lib/posthog/client', () => ({ captureEvent: mockCaptureEvent }))
3032
vi.mock('@sim/utils/random', () => ({ randomFloat: () => 0 }))
3133

3234
vi.mock('@/hooks/queries/credentials', () => ({
33-
useWorkspaceCredentials: () => ({ data: [] }),
35+
useWorkspaceCredentials: () => ({ data: credentialsState.data }),
3436
}))
3537
vi.mock('@/hooks/queries/oauth/oauth-connections', () => ({
36-
useOAuthConnections: () => ({ data: [] }),
38+
useOAuthConnections: () => ({ data: oauthServicesState.data }),
3739
}))
3840
vi.mock('@/hooks/queries/tables', () => ({
3941
useTablesList: () => ({ data: [] }),
@@ -114,6 +116,8 @@ beforeEach(() => {
114116
onSelectPrompt.mockClear()
115117
mockCaptureEvent.mockClear()
116118
modeState.initial = 'build'
119+
credentialsState.data = []
120+
oauthServicesState.data = []
117121
})
118122

119123
afterEach(() => {
@@ -150,4 +154,36 @@ describe('SuggestedActions', () => {
150154
expect(document.querySelector('[data-testid="search-sources"]')).not.toBeNull()
151155
expect(rows()).toHaveLength(0)
152156
})
157+
158+
it('does not offer a service-account-only integration through the OAuth modal', () => {
159+
const OracleIcon = () => null
160+
credentialsState.data = [
161+
{
162+
id: 'existing-oauth',
163+
type: 'oauth',
164+
providerId: 'slack',
165+
},
166+
]
167+
oauthServicesState.data = [
168+
{
169+
id: 'oracle_fusion_scm',
170+
name: 'Oracle Fusion Cloud SCM',
171+
description: 'Read Oracle Fusion Cloud SCM data.',
172+
providerId: 'oracle_fusion_scm',
173+
icon: OracleIcon,
174+
baseProviderIcon: OracleIcon,
175+
scopes: [],
176+
authType: 'service_account',
177+
serviceAccountProviderId: 'oracle-fusion-service-account',
178+
isConnected: false,
179+
},
180+
]
181+
182+
mount()
183+
184+
expect(rows().map((row) => row.textContent)).not.toContain(
185+
'Integrate with Oracle Fusion Cloud SCM'
186+
)
187+
expect(document.querySelector('[data-testid="connect-modal"]')).toBeNull()
188+
})
153189
})

apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ function toIntegrationAction(service: ServiceInfo, slug: string): Action {
178178
*/
179179
function computeActions(services: readonly ServiceInfo[], signals: Signals): Action[] {
180180
const connectCandidates = services.flatMap((s) => {
181+
if ((s.authType ?? 'oauth') !== 'oauth') return []
181182
if (signals.connectedProviders.has(s.providerId)) return []
182183
const slug = SLUG_BY_LOWER_NAME.get(s.name.toLowerCase())
183184
return slug ? [{ service: s, slug }] : []

apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/comp
2727
import {
2828
CONNECT_MODE,
2929
resolveAvailableConnectMode,
30+
resolveOAuthAvailability,
3031
} from '@/app/workspace/[workspaceId]/integrations/connect-route'
3132
import { useScrollRestoration } from '@/app/workspace/[workspaceId]/integrations/hooks/use-scroll-restoration'
3233
import {
@@ -70,7 +71,9 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
7071
const oauthService = resolveOAuthServiceForIntegration(integration)
7172
const { integrationAvailability, isLoading: permissionConfigLoading } = usePermissionConfig()
7273
const availability = integrationAvailability.get(integration.type.toLowerCase())
73-
const oauthAvailable = Boolean(oauthService) && (availability?.oauthAvailable ?? true)
74+
const oauthAvailable =
75+
Boolean(oauthService) &&
76+
resolveOAuthAvailability(availability?.oauthAvailable, integration.authType)
7477
const [oauthOpen, setOAuthOpen] = useState(false)
7578

7679
const { data: credentials = [], isPending: credentialsLoading } = useWorkspaceCredentials({

apps/sim/app/workspace/[workspaceId]/integrations/connect-route.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
22
import {
33
CONNECT_MODE,
44
resolveAvailableConnectMode,
5+
resolveOAuthAvailability,
56
} from '@/app/workspace/[workspaceId]/integrations/connect-route'
67

78
describe('resolveAvailableConnectMode', () => {
@@ -36,4 +37,11 @@ describe('resolveAvailableConnectMode', () => {
3637
})
3738
).toBe(CONNECT_MODE.oauth)
3839
})
40+
41+
it('uses the catalog auth type until deployment availability hydrates', () => {
42+
expect(resolveOAuthAvailability(undefined, 'service-account')).toBe(false)
43+
expect(resolveOAuthAvailability(undefined, 'oauth')).toBe(true)
44+
expect(resolveOAuthAvailability(true, 'service-account')).toBe(true)
45+
expect(resolveOAuthAvailability(false, 'oauth')).toBe(false)
46+
})
3947
})

apps/sim/app/workspace/[workspaceId]/integrations/connect-route.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* the detail page's `?connect=oauth|service-account` query handler.
55
*/
66

7+
import type { AuthType } from '@/lib/integrations/types'
8+
79
export const CONNECT_QUERY_PARAM = 'connect' as const
810

911
export const CONNECT_MODE = {
@@ -29,3 +31,11 @@ export function resolveAvailableConnectMode(
2931
}
3032
return null
3133
}
34+
35+
/** Prevents service-account-only integrations from advertising OAuth before availability loads. */
36+
export function resolveOAuthAvailability(
37+
deployedOAuthAvailable: boolean | undefined,
38+
catalogAuthType: AuthType
39+
): boolean {
40+
return deployedOAuthAvailable ?? catalogAuthType === 'oauth'
41+
}

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/integration-search-items.test.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,44 @@ import { resolveIntegrationSearchConnectMode } from '@/app/workspace/[workspaceI
55
describe('resolveIntegrationSearchConnectMode', () => {
66
it('opens the service-account picker for a ready service-account-only integration', () => {
77
expect(
8-
resolveIntegrationSearchConnectMode({
9-
oauthAvailable: false,
10-
serviceAccountAvailable: true,
11-
})
8+
resolveIntegrationSearchConnectMode(
9+
{
10+
oauthAvailable: false,
11+
serviceAccountAvailable: true,
12+
},
13+
'service-account'
14+
)
1215
).toBe(CONNECT_MODE.serviceAccount)
1316
})
1417

1518
it('prefers OAuth when both stored-credential paths are available', () => {
1619
expect(
17-
resolveIntegrationSearchConnectMode({
18-
oauthAvailable: true,
19-
serviceAccountAvailable: true,
20-
})
20+
resolveIntegrationSearchConnectMode(
21+
{
22+
oauthAvailable: true,
23+
serviceAccountAvailable: true,
24+
},
25+
'oauth'
26+
)
2127
).toBe(CONNECT_MODE.oauth)
2228
})
2329

2430
it('does not request a connection dialog when neither path is available', () => {
2531
expect(
26-
resolveIntegrationSearchConnectMode({
27-
oauthAvailable: false,
28-
serviceAccountAvailable: false,
29-
})
32+
resolveIntegrationSearchConnectMode(
33+
{
34+
oauthAvailable: false,
35+
serviceAccountAvailable: false,
36+
},
37+
'oauth'
38+
)
3039
).toBeNull()
3140
})
41+
42+
it('uses the catalog auth type before deployment availability hydrates', () => {
43+
expect(resolveIntegrationSearchConnectMode(null, 'service-account')).toBe(
44+
CONNECT_MODE.serviceAccount
45+
)
46+
expect(resolveIntegrationSearchConnectMode(null, 'oauth')).toBe(CONNECT_MODE.oauth)
47+
})
3248
})

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/integration-search-items.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ const FALLBACK_BG_COLOR = '#6B7280'
1313

1414
/** Selects the credential dialog that a catalog search result should open. */
1515
export function resolveIntegrationSearchConnectMode(
16-
availability: Pick<IntegrationAvailability, 'oauthAvailable' | 'serviceAccountAvailable'> | null
16+
availability: Pick<IntegrationAvailability, 'oauthAvailable' | 'serviceAccountAvailable'> | null,
17+
authType: 'oauth' | 'service-account'
1718
): (typeof CONNECT_MODE)[keyof typeof CONNECT_MODE] | null {
18-
if (!availability) return CONNECT_MODE.oauth
19+
if (!availability) {
20+
return authType === 'service-account' ? CONNECT_MODE.serviceAccount : CONNECT_MODE.oauth
21+
}
1922
if (availability.oauthAvailable) return CONNECT_MODE.oauth
2023
if (availability.serviceAccountAvailable) return CONNECT_MODE.serviceAccount
2124
return null
@@ -60,13 +63,15 @@ export function buildIntegrationSearchItems(
6063
workspaceId: string,
6164
isBlockAllowed: (blockType: string) => boolean = () => true,
6265
getConnectMode: (
63-
blockType: string
64-
) => (typeof CONNECT_MODE)[keyof typeof CONNECT_MODE] | null = () => CONNECT_MODE.oauth
66+
blockType: string,
67+
authType: 'oauth' | 'service-account'
68+
) => (typeof CONNECT_MODE)[keyof typeof CONNECT_MODE] | null = (_blockType, authType) =>
69+
authType === 'service-account' ? CONNECT_MODE.serviceAccount : CONNECT_MODE.oauth
6570
): IntegrationSearchItem[] {
6671
return INTEGRATION_BASES.filter((base) => isBlockAllowed(base.blockType)).map((base) => {
6772
const connectMode =
6873
base.authType === 'oauth' || base.authType === 'service-account'
69-
? getConnectMode(base.blockType)
74+
? getConnectMode(base.blockType, base.authType)
7075
: null
7176
const connectSuffix = connectMode ? `?${CONNECT_QUERY_PARAM}=${connectMode}` : ''
7277
return {

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,9 +1100,9 @@ export const Sidebar = memo(function Sidebar({
11001100
() =>
11011101
permissionConfig.hideIntegrationsTab
11021102
? []
1103-
: buildIntegrationSearchItems(workspaceId, isBlockAllowed, (blockType) => {
1103+
: buildIntegrationSearchItems(workspaceId, isBlockAllowed, (blockType, authType) => {
11041104
const availability = integrationAvailability.get(blockType.toLowerCase())
1105-
return resolveIntegrationSearchConnectMode(availability ?? null)
1105+
return resolveIntegrationSearchConnectMode(availability ?? null, authType)
11061106
}),
11071107
[workspaceId, permissionConfig.hideIntegrationsTab, isBlockAllowed, integrationAvailability]
11081108
)

apps/sim/blocks/blocks/oracle_fusion_scm.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -472,15 +472,25 @@ Return ONLY the q filter expression - no explanations or extra text.`,
472472
config: {
473473
tool: (params) => params.operation,
474474
params: (params) => {
475-
const { operation: _operation, ...rest } = params
475+
const {
476+
operation: _operation,
477+
advancedMode: _advancedMode,
478+
q,
479+
finder,
480+
orderBy,
481+
limit,
482+
offset,
483+
totalResults,
484+
...rest
485+
} = params
476486
return {
477487
...rest,
478-
q: optionalString(rest.q, 'Filter'),
479-
finder: optionalString(rest.finder, 'Finder'),
480-
orderBy: optionalString(rest.orderBy, 'Order By'),
481-
limit: parseOptionalNumberInput(rest.limit, 'Limit', { integer: true, min: 1, max: 100 }),
482-
offset: parseOptionalNumberInput(rest.offset, 'Offset', { integer: true, min: 0 }),
483-
totalResults: parseOptionalBooleanInput(rest.totalResults),
488+
q: optionalString(q, 'Filter'),
489+
finder: optionalString(finder, 'Finder'),
490+
orderBy: optionalString(orderBy, 'Order By'),
491+
limit: parseOptionalNumberInput(limit, 'Limit', { integer: true, min: 1, max: 100 }),
492+
offset: parseOptionalNumberInput(offset, 'Offset', { integer: true, min: 0 }),
493+
totalResults: parseOptionalBooleanInput(totalResults),
484494
}
485495
},
486496
},

0 commit comments

Comments
 (0)