diff --git a/server/src/__tests__/aws-secrets-manager-provider.test.ts b/server/src/__tests__/aws-secrets-manager-provider.test.ts index c2fc4cb6e17..df6aae4285a 100644 --- a/server/src/__tests__/aws-secrets-manager-provider.test.ts +++ b/server/src/__tests__/aws-secrets-manager-provider.test.ts @@ -231,6 +231,102 @@ describe("awsSecretsManagerProvider", () => { expect(init?.signal).toBeInstanceOf(AbortSignal); }); + it("preserves classified AWS create errors returned by the JSON gateway", async () => { + delete process.env.AWS_PROFILE; + delete process.env.AWS_DEFAULT_PROFILE; + delete process.env.AWS_CONFIG_FILE; + delete process.env.AWS_SHARED_CREDENTIALS_FILE; + delete process.env.AWS_SDK_LOAD_CONFIG; + process.env.AWS_ACCESS_KEY_ID = "AKIA_CREATE_ERROR_TEST"; + process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key"; + + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response( + JSON.stringify({ + __type: "AccessDeniedException", + message: + "User: arn:aws:sts::123456789012:assumed-role/prod/Paperclip is not authorized to perform secretsmanager:CreateSecret", + }), + { status: 400 }, + ), + ); + const provider = createAwsSecretsManagerProvider({ + config: { + region: "us-west-1", + endpoint: "https://secretsmanager.us-west-1.amazonaws.com", + deploymentId: "prod", + prefix: "paperclip", + kmsKeyId: null, + environmentTag: "production", + providerOwnerTag: "paperclip", + deleteRecoveryWindowDays: 30, + }, + }); + + await expect( + provider.createSecret({ + value: "super-secret-value", + context: { + companyId: "company-1", + secretKey: "openai-api-key", + secretName: "OpenAI API Key", + version: 1, + }, + }), + ).rejects.toMatchObject({ + code: "access_denied", + status: 403, + operation: "CreateSecret", + message: "AWS Secrets Manager denied the request. Check IAM permissions for this provider vault.", + rawMessage: expect.stringContaining("AccessDeniedException"), + }); + }); + + it("classifies missing runtime AWS credentials as access denied", async () => { + const provider = createAwsSecretsManagerProvider({ + config: { + region: "us-east-1", + endpoint: "https://secretsmanager.us-east-1.amazonaws.com", + deploymentId: "prod-use1", + prefix: "paperclip", + kmsKeyId: null, + environmentTag: "production", + providerOwnerTag: "paperclip", + deleteRecoveryWindowDays: 30, + }, + gateway: { + async createSecret() { + throw new Error("CredentialsProviderError: Could not load credentials from any providers"); + }, + async putSecretValue() { + throw new Error("not used"); + }, + async getSecretValue() { + throw new Error("not used"); + }, + async deleteSecret() { + throw new Error("not used"); + }, + }, + }); + + await expect( + provider.createSecret({ + value: "super-secret-value", + context: { + companyId: "company-1", + secretKey: "openai-api-key", + secretName: "OpenAI API Key", + version: 1, + }, + }), + ).rejects.toMatchObject({ + code: "access_denied", + status: 403, + rawMessage: "CredentialsProviderError: Could not load credentials from any providers", + }); + }); + it("creates new AWS secret versions against a namespace-valid existing secret reference", async () => { const calls: Array<{ op: string; input: Record }> = []; const provider = createAwsSecretsManagerProvider({ diff --git a/server/src/__tests__/secrets-service.test.ts b/server/src/__tests__/secrets-service.test.ts index e570ec2a6ee..8ad763f14e3 100644 --- a/server/src/__tests__/secrets-service.test.ts +++ b/server/src/__tests__/secrets-service.test.ts @@ -3409,6 +3409,9 @@ describeEmbeddedPostgres("secretService", () => { requiredCapability: "secretsmanager:CreateSecret", }, }); + expect((thrown as HttpError).details?.actionableMessage).toContain( + "Ensure the Paperclip server runtime can resolve valid AWS credentials", + ); expect(JSON.stringify(thrown)).not.toContain("arn:aws"); expect(JSON.stringify(thrown)).not.toContain("123456789012"); expect(thrown instanceof Error ? thrown.message : String(thrown)).not.toContain("arn:aws"); diff --git a/server/src/secrets/aws-secrets-manager-provider.ts b/server/src/secrets/aws-secrets-manager-provider.ts index 87090c86c53..4717217cf38 100644 --- a/server/src/secrets/aws-secrets-manager-provider.ts +++ b/server/src/secrets/aws-secrets-manager-provider.ts @@ -840,7 +840,11 @@ function asAwsSecretsManagerMaterial(value: StoredSecretVersionMaterial): AwsSec function classifyAwsProviderError(message: string): SecretProviderClientErrorCode { if (/ResourceExistsException|AlreadyExists/i.test(message)) return "conflict"; if (/ResourceNotFoundException|NotFound/i.test(message)) return "not_found"; - if (/AccessDeniedException|AccessDenied|UnrecognizedClientException|InvalidClientTokenId|not authorized/i.test(message)) { + if ( + /AccessDeniedException|AccessDenied|CredentialsProviderError|Could not load credentials|credential provider chain|UnrecognizedClientException|InvalidClientTokenId|ExpiredTokenException|InvalidSignatureException|SignatureDoesNotMatch|not authorized/i.test( + message, + ) + ) { return "access_denied"; } if (/Throttl|TooManyRequests|RequestLimitExceeded|Rate exceeded/i.test(message)) return "throttled"; @@ -870,6 +874,9 @@ function awsProviderSafeMessage(code: SecretProviderClientErrorCode): string { } function normalizeAwsError(operation: string, error: unknown): never { + if (error instanceof SecretProviderClientError) { + throw error; + } const rawMessage = error instanceof Error ? error.message : String(error); const code = classifyAwsProviderError(rawMessage); throw new SecretProviderClientError({ diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index 800c0e2fbdd..0fc88ed5421 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -505,7 +505,7 @@ function safeRemoteProviderErrorDetails( if (context.operation === "secret.create") { details.requiredCapability = "secretsmanager:CreateSecret"; details.actionableMessage = - "AWS managed secret creation needs secretsmanager:CreateSecret in the selected region for this provider vault. If the vault config uses a KMS key, the runtime credentials also need KMS write permissions for that key."; + "Ensure the Paperclip server runtime can resolve valid AWS credentials, then grant them secretsmanager:CreateSecret in the selected region for this provider vault. If the vault config uses a KMS key, those credentials also need KMS write permissions for that key."; details.safeAlternative = "If the secret already exists in AWS, link it as an external reference instead of creating a Paperclip-managed value."; } else if (context.operation === "secret.rotate") {