Skip to content

Commit 585c718

Browse files
committed
fix(connectors): sign saved AWS credentials only for commercial and GovCloud S3
China regions serve S3 under amazonaws.com.cn and the isolated partitions under their own domains, so a saved credential for them failed with an opaque DNS error. Refuse them with a clear message instead.
1 parent d6b10aa commit 585c718

4 files changed

Lines changed: 41 additions & 5 deletions

File tree

‎apps/docs/content/docs/knowledgebase/connectors.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ API-key and personal-access-token connections use the credential shown in their
4646
| Azure DevOps | PAT with Project and Team Read, Wiki Read, Work Items Read, and Code Read. |
4747
| GitLab | PAT with `read_api` and access to the selected project; permission-aware methods have additional requirements. |
4848
| YouTube | YouTube Data API key from Google Cloud Console. |
49-
| Amazon S3 | Secret Access Key; enter Access Key ID, region, and bucket as configuration fields. Where enabled, **Authentication → Saved AWS credential** uses a workspace AWS credential instead (access keys, an assumed IAM role, or a keyless role trusted through Sim OIDC); it signs only for Amazon S3 in an AWS region, so it has no Access Key ID or custom endpoint. Either way, the identity needs `s3:ListBucket` on the bucket and `s3:GetObject` on the synced objects. |
49+
| Amazon S3 | Secret Access Key; enter Access Key ID, region, and bucket as configuration fields. Where enabled, **Authentication → Saved AWS credential** uses a workspace AWS credential instead (access keys, an assumed IAM role, or a keyless role trusted through Sim OIDC); it signs only for Amazon S3 in a commercial or GovCloud AWS region (not China or isolated regions), so it has no Access Key ID or custom endpoint. Either way, the identity needs `s3:ListBucket` on the bucket and `s3:GetObject` on the synced objects. |
5050
| Sentry | Auth token with `project:read` and `event:read`. |
5151
| PagerDuty | Create a [REST API key](https://support.pagerduty.com/main/docs/api-access-keys) under **Integrations → Developer Tools → API Access Keys**. |
5252
| SFTP | Password or unencrypted private key, plus a required SHA-256 host-key fingerprint; enter host, port, username, and root path as configuration fields. |

‎apps/sim/connectors/s3/meta.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const s3ConnectorMeta: ConnectorMeta = {
3232
placeholder: 'e.g. us-east-1 (use auto for Cloudflare R2)',
3333
required: true,
3434
description:
35-
'AWS region for the bucket. For Cloudflare R2 use "auto"; for MinIO use the region the server is configured with (commonly us-east-1). A saved AWS credential needs an AWS region.',
35+
'AWS region for the bucket. For Cloudflare R2 use "auto"; for MinIO use the region the server is configured with (commonly us-east-1). A saved AWS credential needs a commercial or GovCloud AWS region.',
3636
},
3737
{
3838
id: 'bucket',

‎apps/sim/connectors/s3/s3.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,36 @@ describe('s3Connector signing', () => {
9797
expect(mockSecureFetchWithRetry).not.toHaveBeenCalled()
9898
})
9999

100+
it.each(['cn-north-1', 'us-iso-east-1', 'eu-isoe-west-1'])(
101+
'refuses %s, whose S3 host is not under amazonaws.com, with a saved AWS credential',
102+
async (region) => {
103+
recordRequests()
104+
105+
const validation = await s3Connector.validateConfig(
106+
'credential-handle',
107+
{ region, bucket: 'docs' },
108+
{ ...awsSessionSyncContext(SESSION) }
109+
)
110+
111+
expect(validation.valid).toBe(false)
112+
expect(validation.error).toMatch(/commercial or GovCloud/i)
113+
expect(mockSecureFetchWithRetry).not.toHaveBeenCalled()
114+
}
115+
)
116+
117+
it('signs a saved AWS credential for a GovCloud bucket', async () => {
118+
const requests = recordRequests()
119+
120+
await s3Connector.listDocuments(
121+
'',
122+
{ region: 'us-gov-west-1', bucket: 'docs-bucket' },
123+
undefined,
124+
{ ...awsSessionSyncContext(SESSION) }
125+
)
126+
127+
expect(requests[0]?.url).toMatch(/^https:\/\/docs-bucket\.s3\.us-gov-west-1\.amazonaws\.com\//)
128+
})
129+
100130
it('keeps signing inline keys without a session token', async () => {
101131
const requests = recordRequests()
102132

‎apps/sim/connectors/s3/s3.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,12 @@ const BUCKET_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,62}$/
5353
/** Accepted region shape — AWS region codes plus Cloudflare R2's `auto`. */
5454
const REGION_PATTERN = /^[A-Za-z0-9-]{1,64}$/
5555

56-
/** An AWS region code (`us-east-1`, `us-iso-east-1`): the only regions a saved AWS credential signs for. */
57-
const AWS_REGION_PATTERN = /^[a-z]{2,}(-[a-z]+)+-\d+$/
56+
/**
57+
* A commercial or GovCloud AWS region code (`us-east-1`, `us-gov-west-1`): the only regions a
58+
* saved AWS credential signs for, because their S3 hosts are all `s3.<region>.amazonaws.com`.
59+
* China (`cn-*`, under `amazonaws.com.cn`) and the isolated partitions use other domains.
60+
*/
61+
const AWS_REGION_PATTERN = /^(?!cn-)[a-z]{2}(-gov)?-[a-z]+-\d+$/
5862

5963
/**
6064
* Default set of file extensions considered safely text-extractable. Objects
@@ -253,7 +257,9 @@ function resolveContext(
253257
)
254258
}
255259
if (!AWS_REGION_PATTERN.test(region)) {
256-
throw new Error('A saved AWS credential needs an AWS region, e.g. us-east-1')
260+
throw new Error(
261+
'A saved AWS credential needs a commercial or GovCloud AWS region, e.g. us-east-1'
262+
)
257263
}
258264
return { ...savedSession, region, bucket }
259265
}

0 commit comments

Comments
 (0)