n8n/packages/nodes-base/credentials/common/aws/utils.test.ts

753 lines
22 KiB
TypeScript

import { ApplicationError } from 'n8n-workflow';
import type { AwsAssumeRoleCredentialsType, AWSRegion } from './types';
global.fetch = jest.fn();
jest.mock('aws4', () => ({
sign: jest.fn(),
}));
jest.mock('xml2js', () => ({
parseString: jest.fn(),
}));
import { sign } from 'aws4';
import { parseString } from 'xml2js';
import { assumeRole } from './utils';
import * as systemCredentialsUtils from './system-credentials-utils';
describe('assumeRole', () => {
let mockFetch: jest.MockedFunction<typeof fetch>;
let mockSign: jest.MockedFunction<typeof sign>;
let mockParseString: jest.MockedFunction<typeof parseString>;
let consoleErrorSpy: jest.SpyInstance;
beforeEach(() => {
jest.clearAllMocks();
mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
mockSign = sign as jest.MockedFunction<typeof sign>;
mockParseString = parseString as jest.MockedFunction<typeof parseString>;
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
mockSign.mockImplementation((request: any) => request as any);
});
afterEach(() => {
consoleErrorSpy.mockRestore();
});
describe('with system credentials', () => {
it('should successfully assume role using system credentials by environment', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: true,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
roleSessionName: 'test-session',
};
const mockSystemCredentials = {
accessKeyId: 'system-access-key',
secretAccessKey: 'system-secret-key',
sessionToken: 'system-session-token',
source: 'environment' as const,
};
jest
.spyOn(systemCredentialsUtils, 'getSystemCredentials')
.mockResolvedValue(mockSystemCredentials);
const mockResponse = {
ok: true,
text: jest.fn().mockResolvedValue(`<?xml version="1.0" encoding="UTF-8"?>
<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleResult>
<Credentials>
<AccessKeyId>assumed-access-key</AccessKeyId>
<SecretAccessKey>assumed-secret-key</SecretAccessKey>
<SessionToken>assumed-session-token</SessionToken>
</Credentials>
</AssumeRoleResult>
</AssumeRoleResponse>`),
};
mockFetch.mockResolvedValue(mockResponse as any);
mockParseString.mockImplementation((_xml, _options, callback) => {
callback(null, {
AssumeRoleResponse: {
AssumeRoleResult: {
Credentials: {
AccessKeyId: 'assumed-access-key',
SecretAccessKey: 'assumed-secret-key',
SessionToken: 'assumed-session-token',
},
},
},
});
});
const result = await assumeRole(credentials, 'us-east-1');
expect(result).toEqual({
accessKeyId: 'assumed-access-key',
secretAccessKey: 'assumed-secret-key',
sessionToken: 'assumed-session-token',
});
expect(systemCredentialsUtils.getSystemCredentials).toHaveBeenCalled();
expect(mockSign).toHaveBeenCalledWith(
expect.objectContaining({
method: 'POST',
path: '/',
region: 'us-east-1',
}),
mockSystemCredentials,
);
expect(mockFetch).toHaveBeenCalledWith(
'https://sts.us-east-1.amazonaws.com',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('Action=AssumeRole'),
}),
);
});
it('should successfully assume role using system credentials by instanceMetadata', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: true,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
roleSessionName: 'test-session',
};
const mockSystemCredentials = {
accessKeyId: 'system-access-key',
secretAccessKey: 'system-secret-key',
sessionToken: 'system-session-token',
source: 'instanceMetadata' as const,
};
jest
.spyOn(systemCredentialsUtils, 'getSystemCredentials')
.mockResolvedValue(mockSystemCredentials);
const mockResponse = {
ok: true,
text: jest.fn().mockResolvedValue(`<?xml version="1.0" encoding="UTF-8"?>
<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleResult>
<Credentials>
<AccessKeyId>assumed-access-key</AccessKeyId>
<SecretAccessKey>assumed-secret-key</SecretAccessKey>
<SessionToken>assumed-session-token</SessionToken>
</Credentials>
</AssumeRoleResult>
</AssumeRoleResponse>`),
};
mockFetch.mockResolvedValue(mockResponse as any);
mockParseString.mockImplementation((_xml, _options, callback) => {
callback(null, {
AssumeRoleResponse: {
AssumeRoleResult: {
Credentials: {
AccessKeyId: 'assumed-access-key',
SecretAccessKey: 'assumed-secret-key',
SessionToken: 'assumed-session-token',
},
},
},
});
});
const result = await assumeRole(credentials, 'us-east-1');
expect(result).toEqual({
accessKeyId: 'assumed-access-key',
secretAccessKey: 'assumed-secret-key',
sessionToken: 'assumed-session-token',
});
expect(systemCredentialsUtils.getSystemCredentials).toHaveBeenCalled();
expect(mockSign).toHaveBeenCalledWith(
expect.objectContaining({
method: 'POST',
path: '/',
region: 'us-east-1',
}),
mockSystemCredentials,
);
expect(mockFetch).toHaveBeenCalledWith(
'https://sts.us-east-1.amazonaws.com',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('Action=AssumeRole'),
}),
);
});
it('should throw error when system credentials are not available', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: true,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
};
jest.spyOn(systemCredentialsUtils, 'getSystemCredentials').mockResolvedValue(null);
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(ApplicationError);
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(
'System AWS credentials are required for role assumption',
);
});
it('should include external ID when provided', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: true,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
roleSessionName: 'test-session',
externalId: 'external-123',
};
const mockSystemCredentials = {
accessKeyId: 'system-access-key',
secretAccessKey: 'system-secret-key',
source: 'environment' as const,
};
jest
.spyOn(systemCredentialsUtils, 'getSystemCredentials')
.mockResolvedValue(mockSystemCredentials);
const mockResponse = {
ok: true,
text: jest.fn().mockResolvedValue('<?xml version="1.0" encoding="UTF-8"?>'),
};
mockFetch.mockResolvedValue(mockResponse as any);
mockParseString.mockImplementation((_xml, _options, callback) => {
callback(null, {
AssumeRoleResponse: {
AssumeRoleResult: {
Credentials: {
AccessKeyId: 'assumed-access-key',
SecretAccessKey: 'assumed-secret-key',
SessionToken: 'assumed-session-token',
},
},
},
});
});
await assumeRole(credentials, 'us-east-1');
expect(mockFetch).toHaveBeenCalledWith(
'https://sts.us-east-1.amazonaws.com',
expect.objectContaining({
body: expect.stringContaining('ExternalId=external-123'),
}),
);
});
});
describe('with manual STS credentials', () => {
it('should successfully assume role using manual STS credentials', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
roleSessionName: 'test-session',
stsAccessKeyId: 'sts-access-key',
stsSecretAccessKey: 'sts-secret-key',
stsSessionToken: 'sts-session-token',
};
const mockResponse = {
ok: true,
text: jest.fn().mockResolvedValue('<?xml version="1.0" encoding="UTF-8"?>'),
};
mockFetch.mockResolvedValue(mockResponse as any);
mockParseString.mockImplementation((_xml, _options, callback) => {
callback(null, {
AssumeRoleResponse: {
AssumeRoleResult: {
Credentials: {
AccessKeyId: 'assumed-access-key',
SecretAccessKey: 'assumed-secret-key',
SessionToken: 'assumed-session-token',
},
},
},
});
});
const result = await assumeRole(credentials, 'us-east-1');
expect(result).toEqual({
accessKeyId: 'assumed-access-key',
secretAccessKey: 'assumed-secret-key',
sessionToken: 'assumed-session-token',
});
expect(mockSign).toHaveBeenCalledWith(
expect.objectContaining({
method: 'POST',
path: '/',
region: 'us-east-1',
}),
{
accessKeyId: 'sts-access-key',
secretAccessKey: 'sts-secret-key',
sessionToken: 'sts-session-token',
},
);
});
it('should work without STS session token', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsAccessKeyId: 'sts-access-key',
stsSecretAccessKey: 'sts-secret-key',
};
const mockResponse = {
ok: true,
text: jest.fn().mockResolvedValue('<?xml version="1.0" encoding="UTF-8"?>'),
};
mockFetch.mockResolvedValue(mockResponse as any);
mockParseString.mockImplementation((_xml, _options, callback) => {
callback(null, {
AssumeRoleResponse: {
AssumeRoleResult: {
Credentials: {
AccessKeyId: 'assumed-access-key',
SecretAccessKey: 'assumed-secret-key',
SessionToken: 'assumed-session-token',
},
},
},
});
});
await assumeRole(credentials, 'us-east-1');
expect(mockSign).toHaveBeenCalledWith(expect.anything(), {
accessKeyId: 'sts-access-key',
secretAccessKey: 'sts-secret-key',
sessionToken: undefined,
});
});
it('should throw error when STS access key ID is missing', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsSecretAccessKey: 'sts-secret-key',
};
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(ApplicationError);
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(
'STS Access Key ID is required when not using system credentials',
);
});
it('should throw error when STS access key ID is empty', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsAccessKeyId: ' ',
stsSecretAccessKey: 'sts-secret-key',
};
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(ApplicationError);
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(
'STS Access Key ID is required when not using system credentials',
);
});
it('should throw error when STS secret access key is missing', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsAccessKeyId: 'sts-access-key',
};
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(ApplicationError);
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(
'STS Secret Access Key is required when not using system credentials',
);
});
it('should throw error when STS secret access key is empty', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsAccessKeyId: 'sts-access-key',
stsSecretAccessKey: ' ',
};
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(ApplicationError);
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(
'STS Secret Access Key is required when not using system credentials',
);
});
it('should trim whitespace from STS credentials', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsAccessKeyId: ' sts-access-key ',
stsSecretAccessKey: ' sts-secret-key ',
stsSessionToken: ' sts-session-token ',
};
const mockResponse = {
ok: true,
text: jest.fn().mockResolvedValue('<?xml version="1.0" encoding="UTF-8"?>'),
};
mockFetch.mockResolvedValue(mockResponse as any);
mockParseString.mockImplementation((_xml, _options, callback) => {
callback(null, {
AssumeRoleResponse: {
AssumeRoleResult: {
Credentials: {
AccessKeyId: 'assumed-access-key',
SecretAccessKey: 'assumed-secret-key',
SessionToken: 'assumed-session-token',
},
},
},
});
});
await assumeRole(credentials, 'us-east-1');
expect(mockSign).toHaveBeenCalledWith(expect.anything(), {
accessKeyId: 'sts-access-key',
secretAccessKey: 'sts-secret-key',
sessionToken: 'sts-session-token',
});
});
});
describe('region handling', () => {
it('should use correct endpoint for China regions', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'cn-north-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws-cn:iam::123456789012:role/TestRole',
stsAccessKeyId: 'sts-access-key',
stsSecretAccessKey: 'sts-secret-key',
};
const mockResponse = {
ok: true,
text: jest.fn().mockResolvedValue('<?xml version="1.0" encoding="UTF-8"?>'),
};
mockFetch.mockResolvedValue(mockResponse as any);
mockParseString.mockImplementation((_xml, _options, callback) => {
callback(null, {
AssumeRoleResponse: {
AssumeRoleResult: {
Credentials: {
AccessKeyId: 'assumed-access-key',
SecretAccessKey: 'assumed-secret-key',
SessionToken: 'assumed-session-token',
},
},
},
});
});
await assumeRole(credentials, 'cn-north-1' as AWSRegion);
expect(mockFetch).toHaveBeenCalledWith(
'https://sts.cn-north-1.amazonaws.com.cn',
expect.any(Object),
);
});
it('should use correct endpoint for standard regions', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'eu-west-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsAccessKeyId: 'sts-access-key',
stsSecretAccessKey: 'sts-secret-key',
};
const mockResponse = {
ok: true,
text: jest.fn().mockResolvedValue('<?xml version="1.0" encoding="UTF-8"?>'),
};
mockFetch.mockResolvedValue(mockResponse as any);
mockParseString.mockImplementation((_xml, _options, callback) => {
callback(null, {
AssumeRoleResponse: {
AssumeRoleResult: {
Credentials: {
AccessKeyId: 'assumed-access-key',
SecretAccessKey: 'assumed-secret-key',
SessionToken: 'assumed-session-token',
},
},
},
});
});
await assumeRole(credentials, 'eu-west-1');
expect(mockFetch).toHaveBeenCalledWith(
'https://sts.eu-west-1.amazonaws.com',
expect.any(Object),
);
});
});
describe('error handling', () => {
it('should throw error when signing fails', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsAccessKeyId: 'sts-access-key',
stsSecretAccessKey: 'sts-secret-key',
};
mockSign.mockImplementation(() => {
throw new Error('Signing failed');
});
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(ApplicationError);
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(
'Failed to sign STS request',
);
});
it('should throw error when STS request fails', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsAccessKeyId: 'sts-access-key',
stsSecretAccessKey: 'sts-secret-key',
};
const mockResponse = {
ok: false,
status: 403,
statusText: 'Forbidden',
text: jest.fn().mockResolvedValue('Access denied'),
};
mockFetch.mockResolvedValue(mockResponse as any);
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(ApplicationError);
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(
'STS AssumeRole failed: 403 Forbidden - Access denied',
);
});
it('should throw error when XML parsing fails', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsAccessKeyId: 'sts-access-key',
stsSecretAccessKey: 'sts-secret-key',
};
const mockResponse = {
ok: true,
text: jest.fn().mockResolvedValue('invalid xml'),
};
mockFetch.mockResolvedValue(mockResponse as any);
mockParseString.mockImplementation((_xml, _options, callback) => {
callback(new Error('XML parsing failed'), null);
});
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow('XML parsing failed');
});
it('should throw error when response has no credentials', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsAccessKeyId: 'sts-access-key',
stsSecretAccessKey: 'sts-secret-key',
};
const mockResponse = {
ok: true,
text: jest.fn().mockResolvedValue('<?xml version="1.0" encoding="UTF-8"?>'),
};
mockFetch.mockResolvedValue(mockResponse as any);
mockParseString.mockImplementation((_xml, _options, callback) => {
callback(null, {
AssumeRoleResponse: {
AssumeRoleResult: {},
},
});
});
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(ApplicationError);
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(
'Invalid response from STS AssumeRole',
);
});
it('should throw error when response structure is invalid', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsAccessKeyId: 'sts-access-key',
stsSecretAccessKey: 'sts-secret-key',
};
const mockResponse = {
ok: true,
text: jest.fn().mockResolvedValue('<?xml version="1.0" encoding="UTF-8"?>'),
};
mockFetch.mockResolvedValue(mockResponse as any);
mockParseString.mockImplementation((_xml, _options, callback) => {
callback(null, {
InvalidResponse: {},
});
});
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(ApplicationError);
await expect(assumeRole(credentials, 'us-east-1')).rejects.toThrow(
'Invalid response from STS AssumeRole',
);
});
});
describe('default values', () => {
it('should use default role session name when not provided', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
useSystemCredentialsForRole: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsAccessKeyId: 'sts-access-key',
stsSecretAccessKey: 'sts-secret-key',
};
const mockResponse = {
ok: true,
text: jest.fn().mockResolvedValue('<?xml version="1.0" encoding="UTF-8"?>'),
};
mockFetch.mockResolvedValue(mockResponse as any);
mockParseString.mockImplementation((_xml, _options, callback) => {
callback(null, {
AssumeRoleResponse: {
AssumeRoleResult: {
Credentials: {
AccessKeyId: 'assumed-access-key',
SecretAccessKey: 'assumed-secret-key',
SessionToken: 'assumed-session-token',
},
},
},
});
});
await assumeRole(credentials, 'us-east-1');
expect(mockFetch).toHaveBeenCalledWith(
'https://sts.us-east-1.amazonaws.com',
expect.objectContaining({
body: expect.stringContaining('RoleSessionName=n8n-session'),
}),
);
});
it('should default useSystemCredentialsForRole to false when not provided', async () => {
const credentials: AwsAssumeRoleCredentialsType = {
region: 'us-east-1',
customEndpoints: false,
roleArn: 'arn:aws:iam::123456789012:role/TestRole',
stsAccessKeyId: 'sts-access-key',
stsSecretAccessKey: 'sts-secret-key',
};
const mockResponse = {
ok: true,
text: jest.fn().mockResolvedValue('<?xml version="1.0" encoding="UTF-8"?>'),
};
mockFetch.mockResolvedValue(mockResponse as any);
mockParseString.mockImplementation((_xml, _options, callback) => {
callback(null, {
AssumeRoleResponse: {
AssumeRoleResult: {
Credentials: {
AccessKeyId: 'assumed-access-key',
SecretAccessKey: 'assumed-secret-key',
SessionToken: 'assumed-session-token',
},
},
},
});
});
await assumeRole(credentials, 'us-east-1');
expect(mockSign).toHaveBeenCalledWith(expect.anything(), {
accessKeyId: 'sts-access-key',
secretAccessKey: 'sts-secret-key',
sessionToken: undefined,
});
});
});
});