Skip to content

Commit 6629800

Browse files
committed
fix(bedrock): refuse an unauthorized saved AWS credential with 401 like Vertex
1 parent 06ed398 commit 6629800

2 files changed

Lines changed: 73 additions & 34 deletions

File tree

‎apps/sim/lib/internal/llm/operations.test.ts‎

Lines changed: 62 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const mocks = vi.hoisted(() => ({
1616
importProvenance: vi.fn(),
1717
isComplete: vi.fn(),
1818
resolveVertexAccessToken: vi.fn(),
19-
resolveBedrockCredentialSession: vi.fn(),
19+
resolveAuthorizedAwsCredential: vi.fn(),
2020
}))
2121

2222
vi.mock('@/providers', () => providersMock)
@@ -28,9 +28,10 @@ vi.mock('@/lib/mothership/environment-context', () => mothershipEnvironmentConte
2828
vi.mock('@/lib/internal/llm/credentials', () => ({
2929
resolveVertexAccessToken: mocks.resolveVertexAccessToken,
3030
}))
31-
vi.mock('@/lib/internal/llm/bedrock-credential', () => ({
32-
resolveBedrockCredentialSession: mocks.resolveBedrockCredentialSession,
31+
vi.mock('@/lib/internal/aws/credentials', () => ({
32+
resolveAuthorizedAwsCredential: mocks.resolveAuthorizedAwsCredential,
3333
}))
34+
vi.mock('@/lib/oauth/token-resolution', () => ({ recordCredentialAccess: vi.fn() }))
3435
vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)
3536
vi.mock('@/executor/utils/resolved-secret-content-projection', () => ({
3637
projectResolvedSecretModelContent: vi.fn(),
@@ -51,6 +52,20 @@ const BILLING_ATTRIBUTION = {
5152
billingEntity: { type: 'user' as const, id: 'user-1' },
5253
}
5354

55+
const BEDROCK_CREDENTIAL_INPUT = {
56+
provider: 'bedrock',
57+
model: 'bedrock/claude',
58+
bedrockCredential: 'aws-cred',
59+
workspaceId: 'workspace-1',
60+
}
61+
62+
const AUTHORIZED_AWS_CREDENTIAL = {
63+
ok: true,
64+
credentialType: 'service_account',
65+
resolvedCredentialId: 'aws-cred',
66+
workspaceId: 'workspace-1',
67+
}
68+
5469
describe('executeLlmProviderOperation', () => {
5570
beforeEach(() => {
5671
permissionsMockFns.mockCheckWorkspaceAccess.mockResolvedValue({ hasAccess: true })
@@ -116,60 +131,74 @@ describe('executeLlmProviderOperation', () => {
116131
})
117132

118133
it("signs a Bedrock call with the selected AWS credential's session for the caller", async () => {
119-
const session = {
120-
credentialId: 'aws-cred',
134+
mocks.authorizeCredential.mockResolvedValueOnce(AUTHORIZED_AWS_CREDENTIAL)
135+
mocks.resolveAuthorizedAwsCredential.mockResolvedValueOnce({
121136
accessKeyId: 'ASIASESSIONKEY000001',
122137
secretAccessKey: 'session-secret',
123138
sessionToken: 'session-token',
124-
}
125-
mocks.resolveBedrockCredentialSession.mockResolvedValueOnce(session)
139+
})
126140

127141
await executeLlmProviderOperation(
128-
{
129-
provider: 'bedrock',
130-
model: 'bedrock/claude',
131-
bedrockCredential: 'aws-cred',
132-
bedrockRegion: 'eu-west-1',
133-
workspaceId: 'workspace-1',
134-
workflowId: 'workflow-1',
135-
},
142+
{ ...BEDROCK_CREDENTIAL_INPUT, bedrockRegion: 'eu-west-1', workflowId: 'workflow-1' },
136143
{ actorUserId: 'user-1', headers: new Headers(), requestId: 'request-1' }
137144
)
138145

139-
expect(mocks.resolveBedrockCredentialSession).toHaveBeenCalledWith(
146+
expect(mocks.authorizeCredential).toHaveBeenCalledWith(
147+
expect.objectContaining({ userId: 'user-1' }),
140148
expect.objectContaining({
141149
credentialId: 'aws-cred',
142-
actingUserId: 'user-1',
143150
workflowId: 'workflow-1',
144-
workspaceId: 'workspace-1',
151+
callerUserId: 'user-1',
145152
})
146153
)
147154
expect(mockExecuteProviderRequest).toHaveBeenCalledWith(
148155
'bedrock',
149-
expect.objectContaining({ bedrockSession: session, bedrockRegion: 'eu-west-1' }),
156+
expect.objectContaining({
157+
bedrockSession: {
158+
credentialId: 'aws-cred',
159+
accessKeyId: 'ASIASESSIONKEY000001',
160+
secretAccessKey: 'session-secret',
161+
sessionToken: 'session-token',
162+
},
163+
bedrockRegion: 'eu-west-1',
164+
}),
150165
expect.anything()
151166
)
152167
})
153168

154-
it('fails a Bedrock call whose AWS credential cannot be used before provider work', async () => {
169+
it('refuses a Bedrock credential the caller may not use with the status a Vertex refusal gets', async () => {
155170
mockExecuteProviderRequest.mockClear()
156-
mocks.resolveBedrockCredentialSession.mockRejectedValueOnce(
157-
new Error('Not authorized to use this AWS credential')
158-
)
171+
mocks.authorizeCredential.mockResolvedValue({ ok: false, error: 'Credential access denied' })
172+
173+
for (const input of [
174+
BEDROCK_CREDENTIAL_INPUT,
175+
{ provider: 'vertex', model: 'vertex/gemini-2.5-pro', vertexCredential: 'credential-1' },
176+
]) {
177+
await expect(
178+
executeLlmProviderOperation(input, {
179+
actorUserId: 'user-1',
180+
headers: new Headers(),
181+
requestId: 'request-1',
182+
})
183+
).rejects.toMatchObject({ status: 401, body: { error: 'Credential access denied' } })
184+
}
185+
expect(mockExecuteProviderRequest).not.toHaveBeenCalled()
186+
})
187+
188+
it('fails a Bedrock call whose AWS credential cannot be resolved before provider work', async () => {
189+
mockExecuteProviderRequest.mockClear()
190+
mocks.authorizeCredential.mockResolvedValueOnce(AUTHORIZED_AWS_CREDENTIAL)
191+
mocks.resolveAuthorizedAwsCredential.mockRejectedValueOnce(new Error('AccessDenied'))
159192

160193
await expect(
161-
executeLlmProviderOperation(
162-
{
163-
provider: 'bedrock',
164-
model: 'bedrock/claude',
165-
bedrockCredential: 'aws-cred',
166-
workspaceId: 'workspace-1',
167-
},
168-
{ actorUserId: 'user-1', headers: new Headers(), requestId: 'request-1' }
169-
)
194+
executeLlmProviderOperation(BEDROCK_CREDENTIAL_INPUT, {
195+
actorUserId: 'user-1',
196+
headers: new Headers(),
197+
requestId: 'request-1',
198+
})
170199
).rejects.toMatchObject({
171200
status: 400,
172-
body: { error: 'Not authorized to use this AWS credential' },
201+
body: { error: 'Could not use the AWS credential: AccessDenied' },
173202
})
174203
expect(mockExecuteProviderRequest).not.toHaveBeenCalled()
175204
})

‎apps/sim/lib/internal/llm/operations.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import {
1212
inspectModelInputProjectionState,
1313
inspectModelInputProvenanceRequest,
1414
} from '@/lib/execution/model-input-provenance'
15-
import { resolveBedrockCredentialSession } from '@/lib/internal/llm/bedrock-credential'
15+
import {
16+
BedrockCredentialError,
17+
resolveBedrockCredentialSession,
18+
} from '@/lib/internal/llm/bedrock-credential'
1619
import { resolveVertexAccessToken } from '@/lib/internal/llm/credentials'
1720
import { LlmOperationError } from '@/lib/internal/llm/errors'
1821
import type { LlmProviderOperationInput } from '@/lib/internal/llm/input'
@@ -142,6 +145,13 @@ async function prepareProviderRequest(
142145
}
143146
} catch (error) {
144147
if (error instanceof LlmOperationError) throw error
148+
if (error instanceof BedrockCredentialError && error.reason === 'unauthorized') {
149+
logger.warn('Bedrock credential access denied', {
150+
requestId: context.requestId,
151+
credentialId: input.bedrockCredential,
152+
})
153+
fail(401, error.message)
154+
}
145155
logger.error('Failed to resolve provider credential', {
146156
requestId: context.requestId,
147157
provider: input.provider,

0 commit comments

Comments
 (0)