mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-04 18:49:20 +02:00
fix(HTTP Request Node): Sign Amazon Bedrock requests as 'bedrock' service (#31250)
This commit is contained in:
parent
6bcd02a5f0
commit
9963143c57
|
|
@ -79,6 +79,37 @@ export function parseAwsUrl(url: URL): { region: AWSRegion | null; service: stri
|
|||
return { service, region };
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an AWS endpoint subdomain to its SigV4 signing service name.
|
||||
*
|
||||
* Most AWS endpoints sign with the same name as their hostname label, but
|
||||
* some service families (notably Amazon Bedrock) expose multiple endpoint
|
||||
* subdomains that all sign against a single `signingName`. Without this
|
||||
* mapping, `aws4` would derive the signing name from the host and AWS would
|
||||
* reject the request with `SignatureDoesNotMatch`.
|
||||
*
|
||||
* Endpoints that already match their signing name fall through unchanged.
|
||||
*
|
||||
* @param service - Service name as extracted from the endpoint hostname
|
||||
* @returns The SigV4 signing service name
|
||||
*/
|
||||
function getAwsSigningService(service: string): string {
|
||||
switch (service) {
|
||||
// Mirror AWS SDK Bedrock signing for HTTP Request node AWS credentials:
|
||||
// these endpoint families are signed with the `bedrock` service namespace.
|
||||
// https://docs.aws.amazon.com/bedrock/latest/APIReference/welcome.html#API_Reference_Endpoints
|
||||
// https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonbedrock.html
|
||||
case 'bedrock-runtime':
|
||||
case 'bedrock-agent':
|
||||
case 'bedrock-agent-runtime':
|
||||
case 'bedrock-data-automation':
|
||||
case 'bedrock-data-automation-runtime':
|
||||
return 'bedrock';
|
||||
default:
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AWS credentials test configuration for validating AWS credentials.
|
||||
* Uses the STS GetCallerIdentity action to verify that the provided credentials are valid.
|
||||
|
|
@ -219,6 +250,8 @@ export function awsGetSignInOptionsAndUpdateRequest(
|
|||
bodyContent = params.toString();
|
||||
contentTypeHeader = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
|
||||
const signingService = getAwsSigningService(service);
|
||||
const signOpts = {
|
||||
...requestOptions,
|
||||
headers: {
|
||||
|
|
@ -230,6 +263,7 @@ export function awsGetSignInOptionsAndUpdateRequest(
|
|||
path,
|
||||
body: bodyContent,
|
||||
region,
|
||||
...(signingService !== service && { service: signingService }),
|
||||
} as unknown as Request;
|
||||
|
||||
return { signOpts, url: endpoint.origin + path };
|
||||
|
|
|
|||
|
|
@ -144,6 +144,51 @@ describe('Aws Credential', () => {
|
|||
expect(result.url).toBe('https://iam.amazonaws.com/');
|
||||
});
|
||||
|
||||
describe('Amazon Bedrock services', () => {
|
||||
it.each([
|
||||
{
|
||||
host: 'bedrock-runtime.us-east-1.amazonaws.com',
|
||||
path: '/model/anthropic.claude-v2/invoke',
|
||||
},
|
||||
{
|
||||
host: 'bedrock-agent.us-east-1.amazonaws.com',
|
||||
path: '/agents/',
|
||||
},
|
||||
{
|
||||
host: 'bedrock-agent-runtime.us-east-1.amazonaws.com',
|
||||
path: '/agents/agent-id/agentAliases/alias-id/sessions/session-id/text',
|
||||
},
|
||||
{
|
||||
host: 'bedrock-data-automation.us-east-1.amazonaws.com',
|
||||
path: '/projects/',
|
||||
},
|
||||
{
|
||||
host: 'bedrock-data-automation-runtime.us-east-1.amazonaws.com',
|
||||
path: '/invocations',
|
||||
},
|
||||
])(
|
||||
'should sign $host requests with the Bedrock service namespace',
|
||||
async ({ host, path }) => {
|
||||
const result = await aws.authenticate(credentials, {
|
||||
...requestOptions,
|
||||
baseURL: '',
|
||||
url: `https://${host}${path}`,
|
||||
});
|
||||
|
||||
expect(mockSign).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
host,
|
||||
path,
|
||||
region: 'us-east-1',
|
||||
service: 'bedrock',
|
||||
}),
|
||||
securityHeaders,
|
||||
);
|
||||
expect(result.url).toBe(`https://${host}${path}`);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle an IRequestOptions object with form instead of body', async () => {
|
||||
const result = await aws.authenticate({ ...credentials }, {
|
||||
...requestOptions,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { sign } from 'aws4';
|
||||
import type { IHttpRequestOptions } from 'n8n-workflow';
|
||||
|
||||
import { AwsAssumeRole } from '../AwsAssumeRole.credentials';
|
||||
import type { AwsAssumeRoleCredentialsType } from '../common/aws/types';
|
||||
|
||||
jest.mock('aws4', () => ({
|
||||
sign: jest.fn(),
|
||||
}));
|
||||
|
||||
const stsAssumeRoleResponseXml = `<?xml version="1.0"?>
|
||||
<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
|
||||
<AssumeRoleResult>
|
||||
<Credentials>
|
||||
<AccessKeyId>ASSUMED-KEY</AccessKeyId>
|
||||
<SecretAccessKey>ASSUMED-SECRET</SecretAccessKey>
|
||||
<SessionToken>ASSUMED-SESSION</SessionToken>
|
||||
<Expiration>2099-01-01T00:00:00Z</Expiration>
|
||||
</Credentials>
|
||||
</AssumeRoleResult>
|
||||
</AssumeRoleResponse>`;
|
||||
|
||||
describe('AwsAssumeRole Credential', () => {
|
||||
const aws = new AwsAssumeRole();
|
||||
let mockSign: jest.Mock;
|
||||
let mockFetch: jest.SpyInstance;
|
||||
|
||||
const credentials: AwsAssumeRoleCredentialsType = {
|
||||
region: 'us-east-1',
|
||||
customEndpoints: false,
|
||||
roleArn: 'arn:aws:iam::123456789012:role/MyRole',
|
||||
externalId: 'ext-id',
|
||||
roleSessionName: 'n8n-session',
|
||||
stsAccessKeyId: 'sts-key',
|
||||
stsSecretAccessKey: 'sts-secret',
|
||||
useSystemCredentialsForRole: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockSign = sign as unknown as jest.Mock;
|
||||
mockFetch = jest
|
||||
.spyOn(global, 'fetch')
|
||||
.mockResolvedValue(new Response(stsAssumeRoleResponseXml, { status: 200 }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockFetch.mockRestore();
|
||||
});
|
||||
|
||||
it('should sign Bedrock requests with the bedrock service namespace', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
qs: {},
|
||||
body: {},
|
||||
headers: {},
|
||||
baseURL: '',
|
||||
url: 'https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/invoke',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
await aws.authenticate(credentials, requestOptions);
|
||||
|
||||
const finalCall = mockSign.mock.calls.at(-1);
|
||||
expect(finalCall?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
host: 'bedrock-runtime.us-east-1.amazonaws.com',
|
||||
region: 'us-east-1',
|
||||
service: 'bedrock',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user