diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts index b5d94112a..4b86af323 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts @@ -15,6 +15,7 @@ import { z } from "zod"; import { CustomAWSHasher } from "@app/lib/aws/hashing"; import { crypto } from "@app/lib/crypto"; import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models"; @@ -170,14 +171,29 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => { }; const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - await ElastiCacheUserManager( - { - accessKeyId: providerInputs.accessKeyId, - secretAccessKey: providerInputs.secretAccessKey - }, - providerInputs.region - ).verifyCredentials(providerInputs.clusterName); - return true; + try { + await ElastiCacheUserManager( + { + accessKeyId: providerInputs.accessKeyId, + secretAccessKey: providerInputs.secretAccessKey + }, + providerInputs.region + ).verifyCredentials(providerInputs.clusterName); + return true; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [ + providerInputs.accessKeyId, + providerInputs.secretAccessKey, + providerInputs.clusterName, + providerInputs.region + ] + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; const create = async (data: { @@ -206,21 +222,37 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => { const parsedStatement = CreateElastiCacheUserSchema.parse(JSON.parse(creationStatement)); - await ElastiCacheUserManager( - { - accessKeyId: providerInputs.accessKeyId, - secretAccessKey: providerInputs.secretAccessKey - }, - providerInputs.region - ).createUser(parsedStatement, providerInputs.clusterName); + try { + await ElastiCacheUserManager( + { + accessKeyId: providerInputs.accessKeyId, + secretAccessKey: providerInputs.secretAccessKey + }, + providerInputs.region + ).createUser(parsedStatement, providerInputs.clusterName); - return { - entityId: leaseUsername, - data: { - DB_USERNAME: leaseUsername, - DB_PASSWORD: leasePassword - } - }; + return { + entityId: leaseUsername, + data: { + DB_USERNAME: leaseUsername, + DB_PASSWORD: leasePassword + } + }; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [ + leaseUsername, + leasePassword, + providerInputs.accessKeyId, + providerInputs.secretAccessKey, + providerInputs.clusterName + ] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); + } }; const revoke = async (inputs: unknown, entityId: string) => { @@ -229,15 +261,25 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => { const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username: entityId }); const parsedStatement = DeleteElasticCacheUserSchema.parse(JSON.parse(revokeStatement)); - await ElastiCacheUserManager( - { - accessKeyId: providerInputs.accessKeyId, - secretAccessKey: providerInputs.secretAccessKey - }, - providerInputs.region - ).deleteUser(parsedStatement); + try { + await ElastiCacheUserManager( + { + accessKeyId: providerInputs.accessKeyId, + secretAccessKey: providerInputs.secretAccessKey + }, + providerInputs.region + ).deleteUser(parsedStatement); - return { entityId }; + return { entityId }; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [entityId, providerInputs.accessKeyId, providerInputs.secretAccessKey, providerInputs.clusterName] + }); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` + }); + } }; const renew = async (_inputs: unknown, entityId: string) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts index 7bb11b9ae..d4fceb674 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts @@ -23,6 +23,7 @@ import { CustomAWSHasher } from "@app/lib/aws/hashing"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models"; @@ -118,22 +119,39 @@ export const AwsIamProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => { const providerInputs = await validateProviderInputs(inputs); - const client = await $getClient(providerInputs, projectId); - const isConnected = await client - .send(new GetUserCommand({})) - .then(() => true) - .catch((err) => { - const message = (err as Error)?.message; - if ( - (providerInputs.method === AwsIamAuthType.AssumeRole || providerInputs.method === AwsIamAuthType.IRSA) && - // assume role will throw an error asking to provider username, but if so this has access in aws correctly - message.includes("Must specify userName when calling with non-User credentials") - ) { - return true; - } - throw err; + try { + const client = await $getClient(providerInputs, projectId); + const isConnected = await client + .send(new GetUserCommand({})) + .then(() => true) + .catch((err) => { + const message = (err as Error)?.message; + if ( + (providerInputs.method === AwsIamAuthType.AssumeRole || providerInputs.method === AwsIamAuthType.IRSA) && + // assume role will throw an error asking to provider username, but if so this has access in aws correctly + message.includes("Must specify userName when calling with non-User credentials") + ) { + return true; + } + throw err; + }); + return isConnected; + } catch (err) { + const sensitiveTokens = []; + if (providerInputs.method === AwsIamAuthType.AccessKey) { + sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey); + } + if (providerInputs.method === AwsIamAuthType.AssumeRole) { + sensitiveTokens.push(providerInputs.roleArn); + } + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: sensitiveTokens }); - return isConnected; + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; const create = async (data: { @@ -162,62 +180,81 @@ export const AwsIamProvider = (): TDynamicProviderFns => { awsTags.push(...additionalTags); } - const createUserRes = await client.send( - new CreateUserCommand({ - Path: awsPath, - PermissionsBoundary: permissionBoundaryPolicyArn || undefined, - Tags: awsTags, - UserName: username - }) - ); - - if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" }); - if (userGroups) { - await Promise.all( - userGroups - .split(",") - .filter(Boolean) - .map((group) => - client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group })) - ) - ); - } - if (policyArns) { - await Promise.all( - policyArns - .split(",") - .filter(Boolean) - .map((policyArn) => - client.send(new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn })) - ) - ); - } - if (policyDocument) { - await client.send( - new PutUserPolicyCommand({ - UserName: createUserRes.User.UserName, - PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`, - PolicyDocument: policyDocument + try { + const createUserRes = await client.send( + new CreateUserCommand({ + Path: awsPath, + PermissionsBoundary: permissionBoundaryPolicyArn || undefined, + Tags: awsTags, + UserName: username }) ); - } - const createAccessKeyRes = await client.send( - new CreateAccessKeyCommand({ - UserName: createUserRes.User.UserName - }) - ); - if (!createAccessKeyRes.AccessKey) - throw new BadRequestError({ message: "Failed to create AWS IAM User access key" }); - - return { - entityId: username, - data: { - ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId, - SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey, - USERNAME: username + if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" }); + if (userGroups) { + await Promise.all( + userGroups + .split(",") + .filter(Boolean) + .map((group) => + client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group })) + ) + ); } - }; + if (policyArns) { + await Promise.all( + policyArns + .split(",") + .filter(Boolean) + .map((policyArn) => + client.send( + new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn }) + ) + ) + ); + } + if (policyDocument) { + await client.send( + new PutUserPolicyCommand({ + UserName: createUserRes.User.UserName, + PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`, + PolicyDocument: policyDocument + }) + ); + } + + const createAccessKeyRes = await client.send( + new CreateAccessKeyCommand({ + UserName: createUserRes.User.UserName + }) + ); + if (!createAccessKeyRes.AccessKey) + throw new BadRequestError({ message: "Failed to create AWS IAM User access key" }); + + return { + entityId: username, + data: { + ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId, + SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey, + USERNAME: username + } + }; + } catch (err) { + const sensitiveTokens = [username]; + if (providerInputs.method === AwsIamAuthType.AccessKey) { + sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey); + } + if (providerInputs.method === AwsIamAuthType.AssumeRole) { + sensitiveTokens.push(providerInputs.roleArn); + } + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: sensitiveTokens + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); + } }; const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => { @@ -278,8 +315,25 @@ export const AwsIamProvider = (): TDynamicProviderFns => { ) ); - await client.send(new DeleteUserCommand({ UserName: username })); - return { entityId: username }; + try { + await client.send(new DeleteUserCommand({ UserName: username })); + return { entityId: username }; + } catch (err) { + const sensitiveTokens = [username]; + if (providerInputs.method === AwsIamAuthType.AccessKey) { + sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey); + } + if (providerInputs.method === AwsIamAuthType.AssumeRole) { + sensitiveTokens.push(providerInputs.roleArn); + } + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: sensitiveTokens + }); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` + }); + } }; const renew = async (_inputs: unknown, entityId: string) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts b/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts index 4b2232bc8..98386c4d3 100644 --- a/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts +++ b/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts @@ -2,6 +2,7 @@ import axios from "axios"; import { customAlphabet } from "nanoid"; import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { AzureEntraIDSchema, TDynamicProviderFns } from "./models"; @@ -51,45 +52,82 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); - return data.success; + try { + const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); + return data.success; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.clientSecret, providerInputs.applicationId, providerInputs.tenantId] + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; const create = async ({ inputs }: { inputs: unknown }) => { const providerInputs = await validateProviderInputs(inputs); - const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); - if (!data.success) { - throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" }); - } const password = generatePassword(); - - const response = await axios.patch( - `${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`, - { - passwordProfile: { - forceChangePasswordNextSignIn: false, - password - } - }, - { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${data.token}` - } + try { + const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); + if (!data.success) { + throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" }); } - ); - if (response.status !== 204) { - throw new BadRequestError({ message: "Failed to update password" }); - } - return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } }; + const response = await axios.patch( + `${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`, + { + passwordProfile: { + forceChangePasswordNextSignIn: false, + password + } + }, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${data.token}` + } + } + ); + if (response.status !== 204) { + throw new BadRequestError({ message: "Failed to update password" }); + } + + return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } }; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [ + providerInputs.clientSecret, + providerInputs.applicationId, + providerInputs.userId, + providerInputs.email, + password + ] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); + } }; const revoke = async (inputs: unknown, entityId: string) => { - // Creates a new password - await create({ inputs }); - return { entityId }; + const providerInputs = await validateProviderInputs(inputs); + try { + // Creates a new password + await create({ inputs }); + return { entityId }; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.clientSecret, providerInputs.applicationId, entityId] + }); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` + }); + } }; const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts index b939dcad6..294a9a723 100644 --- a/backend/src/ee/services/dynamic-secret/providers/cassandra.ts +++ b/backend/src/ee/services/dynamic-secret/providers/cassandra.ts @@ -3,6 +3,8 @@ import handlebars from "handlebars"; import { customAlphabet } from "nanoid"; import { z } from "zod"; +import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -71,9 +73,24 @@ export const CassandraProvider = (): TDynamicProviderFns => { const providerInputs = await validateProviderInputs(inputs); const client = await $getClient(providerInputs); - const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true); - await client.shutdown(); - return isConnected; + try { + const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true); + await client.shutdown(); + return isConnected; + } catch (err) { + const tokens = [providerInputs.password, providerInputs.username]; + if (providerInputs.keyspace) { + tokens.push(providerInputs.keyspace); + } + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens + }); + await client.shutdown(); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; const create = async (data: { @@ -89,23 +106,39 @@ export const CassandraProvider = (): TDynamicProviderFns => { const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); const { keyspace } = providerInputs; - const expiration = new Date(expireAt).toISOString(); - const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ - username, - password, - expiration, - keyspace - }); + try { + const expiration = new Date(expireAt).toISOString(); - const queries = creationStatement.toString().split(";").filter(Boolean); - for (const query of queries) { - // eslint-disable-next-line - await client.execute(query); + const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ + username, + password, + expiration, + keyspace + }); + + const queries = creationStatement.toString().split(";").filter(Boolean); + for (const query of queries) { + // eslint-disable-next-line + await client.execute(query); + } + await client.shutdown(); + + return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + } catch (err) { + const tokens = [username, password]; + if (keyspace) { + tokens.push(keyspace); + } + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens + }); + await client.shutdown(); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); } - await client.shutdown(); - - return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; }; const revoke = async (inputs: unknown, entityId: string) => { @@ -115,14 +148,29 @@ export const CassandraProvider = (): TDynamicProviderFns => { const username = entityId; const { keyspace } = providerInputs; - const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace }); - const queries = revokeStatement.toString().split(";").filter(Boolean); - for (const query of queries) { - // eslint-disable-next-line - await client.execute(query); + try { + const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace }); + const queries = revokeStatement.toString().split(";").filter(Boolean); + for (const query of queries) { + // eslint-disable-next-line + await client.execute(query); + } + await client.shutdown(); + return { entityId: username }; + } catch (err) { + const tokens = [username]; + if (keyspace) { + tokens.push(keyspace); + } + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens + }); + await client.shutdown(); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` + }); } - await client.shutdown(); - return { entityId: username }; }; const renew = async (inputs: unknown, entityId: string, expireAt: number) => { @@ -130,21 +178,36 @@ export const CassandraProvider = (): TDynamicProviderFns => { if (!providerInputs.renewStatement) return { entityId }; const client = await $getClient(providerInputs); - - const expiration = new Date(expireAt).toISOString(); const { keyspace } = providerInputs; - const renewStatement = handlebars.compile(providerInputs.renewStatement)({ - username: entityId, - keyspace, - expiration - }); - const queries = renewStatement.toString().split(";").filter(Boolean); - for await (const query of queries) { - await client.execute(query); + try { + const expiration = new Date(expireAt).toISOString(); + + const renewStatement = handlebars.compile(providerInputs.renewStatement)({ + username: entityId, + keyspace, + expiration + }); + const queries = renewStatement.toString().split(";").filter(Boolean); + for await (const query of queries) { + await client.execute(query); + } + await client.shutdown(); + return { entityId }; + } catch (err) { + const tokens = [entityId]; + if (keyspace) { + tokens.push(keyspace); + } + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens + }); + await client.shutdown(); + throw new BadRequestError({ + message: `Failed to renew lease from provider: ${sanitizedErrorMessage}` + }); } - await client.shutdown(); - return { entityId }; }; return { diff --git a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts index 32d21ee76..bde62fc61 100644 --- a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -2,6 +2,8 @@ import { Client as ElasticSearchClient } from "@elastic/elasticsearch"; import { customAlphabet } from "nanoid"; import { z } from "zod"; +import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; @@ -63,12 +65,24 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { const providerInputs = await validateProviderInputs(inputs); const connection = await $getClient(providerInputs); - const infoResponse = await connection - .info() - .then(() => true) - .catch(() => false); - - return infoResponse; + try { + const infoResponse = await connection.info().then(() => true); + return infoResponse; + } catch (err) { + const tokens = []; + if (providerInputs.auth.type === ElasticSearchAuthTypes.ApiKey) { + tokens.push(providerInputs.auth.apiKey, providerInputs.auth.apiKeyId); + } else { + tokens.push(providerInputs.auth.username, providerInputs.auth.password); + } + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => { @@ -79,27 +93,49 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => { const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); - await connection.security.putUser({ - username, - password, - full_name: "Managed by Infisical.com", - roles: providerInputs.roles - }); + try { + await connection.security.putUser({ + username, + password, + full_name: "Managed by Infisical.com", + roles: providerInputs.roles + }); - await connection.close(); - return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + await connection.close(); + return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, password] + }); + await connection.close(); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); + } }; const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); const connection = await $getClient(providerInputs); - await connection.security.deleteUser({ - username: entityId - }); + try { + await connection.security.deleteUser({ + username: entityId + }); - await connection.close(); - return { entityId }; + await connection.close(); + return { entityId }; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [entityId] + }); + await connection.close(); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` + }); + } }; const renew = async (_inputs: unknown, entityId: string) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/gcp-iam.ts b/backend/src/ee/services/dynamic-secret/providers/gcp-iam.ts index b5d34aa49..b1459aa4e 100644 --- a/backend/src/ee/services/dynamic-secret/providers/gcp-iam.ts +++ b/backend/src/ee/services/dynamic-secret/providers/gcp-iam.ts @@ -3,6 +3,7 @@ import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth import { getConfig } from "@app/lib/config/env"; import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { DynamicSecretGcpIamSchema, TDynamicProviderFns } from "./models"; @@ -65,8 +66,18 @@ export const GcpIamProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - await $getToken(providerInputs.serviceAccountEmail, 10); - return true; + try { + await $getToken(providerInputs.serviceAccountEmail, 10); + return true; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.serviceAccountEmail] + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; const create = async (data: { inputs: unknown; expireAt: number }) => { @@ -74,13 +85,23 @@ export const GcpIamProvider = (): TDynamicProviderFns => { const providerInputs = await validateProviderInputs(inputs); - const now = Math.floor(Date.now() / 1000); - const ttl = Math.max(Math.floor(expireAt / 1000) - now, 0); + try { + const now = Math.floor(Date.now() / 1000); + const ttl = Math.max(Math.floor(expireAt / 1000) - now, 0); - const token = await $getToken(providerInputs.serviceAccountEmail, ttl); - const entityId = alphaNumericNanoId(32); + const token = await $getToken(providerInputs.serviceAccountEmail, ttl); + const entityId = alphaNumericNanoId(32); - return { entityId, data: { SERVICE_ACCOUNT_EMAIL: providerInputs.serviceAccountEmail, TOKEN: token } }; + return { entityId, data: { SERVICE_ACCOUNT_EMAIL: providerInputs.serviceAccountEmail, TOKEN: token } }; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.serviceAccountEmail] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); + } }; const revoke = async (_inputs: unknown, entityId: string) => { @@ -89,10 +110,21 @@ export const GcpIamProvider = (): TDynamicProviderFns => { }; const renew = async (inputs: unknown, entityId: string, expireAt: number) => { - // To renew a token it must be re-created - const data = await create({ inputs, expireAt }); + try { + // To renew a token it must be re-created + const data = await create({ inputs, expireAt }); - return { ...data, entityId }; + return { ...data, entityId }; + } catch (err) { + const providerInputs = await validateProviderInputs(inputs); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.serviceAccountEmail] + }); + throw new BadRequestError({ + message: `Failed to renew lease from provider: ${sanitizedErrorMessage}` + }); + } }; return { diff --git a/backend/src/ee/services/dynamic-secret/providers/github.ts b/backend/src/ee/services/dynamic-secret/providers/github.ts index 8bd1cccf4..d3259064a 100644 --- a/backend/src/ee/services/dynamic-secret/providers/github.ts +++ b/backend/src/ee/services/dynamic-secret/providers/github.ts @@ -3,6 +3,7 @@ import jwt from "jsonwebtoken"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; @@ -89,26 +90,46 @@ export const GithubProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - await $generateGitHubInstallationAccessToken(providerInputs); - return true; + try { + await $generateGitHubInstallationAccessToken(providerInputs); + return true; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.privateKey, String(providerInputs.appId), String(providerInputs.installationId)] + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; const create = async (data: { inputs: unknown }) => { const { inputs } = data; const providerInputs = await validateProviderInputs(inputs); - const ghTokenData = await $generateGitHubInstallationAccessToken(providerInputs); - const entityId = alphaNumericNanoId(32); + try { + const ghTokenData = await $generateGitHubInstallationAccessToken(providerInputs); + const entityId = alphaNumericNanoId(32); - return { - entityId, - data: { - TOKEN: ghTokenData.token, - EXPIRES_AT: ghTokenData.expires_at, - PERMISSIONS: ghTokenData.permissions, - REPOSITORY_SELECTION: ghTokenData.repository_selection - } - }; + return { + entityId, + data: { + TOKEN: ghTokenData.token, + EXPIRES_AT: ghTokenData.expires_at, + PERMISSIONS: ghTokenData.permissions, + REPOSITORY_SELECTION: ghTokenData.repository_selection + } + }; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.privateKey, String(providerInputs.appId), String(providerInputs.installationId)] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); + } }; const revoke = async () => { diff --git a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts index afc4804b6..3d69c3282 100644 --- a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts +++ b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts @@ -2,7 +2,8 @@ import axios, { AxiosError } from "axios"; import handlebars from "handlebars"; import https from "https"; -import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; @@ -356,8 +357,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): errorMessage = (error.response?.data as { message: string }).message; } - throw new InternalServerError({ - message: `Failed to validate connection: ${errorMessage}` + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: errorMessage, + tokens: [providerInputs.clusterToken || ""] + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` }); } }; @@ -602,8 +607,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): errorMessage = (error.response?.data as { message: string }).message; } - throw new InternalServerError({ - message: `Failed to create dynamic secret: ${errorMessage}` + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: errorMessage, + tokens: [providerInputs.clusterToken || ""] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` }); } }; @@ -683,50 +692,65 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): }; if (providerInputs.credentialType === KubernetesCredentialType.Dynamic) { - const rawUrl = - providerInputs.authMethod === KubernetesAuthMethod.Gateway - ? GATEWAY_AUTH_DEFAULT_URL - : providerInputs.url || ""; + try { + const rawUrl = + providerInputs.authMethod === KubernetesAuthMethod.Gateway + ? GATEWAY_AUTH_DEFAULT_URL + : providerInputs.url || ""; - const url = new URL(rawUrl); - const k8sGatewayHost = url.hostname; - const k8sPort = url.port ? Number(url.port) : 443; - const k8sHost = `${url.protocol}//${url.hostname}`; + const url = new URL(rawUrl); + const k8sGatewayHost = url.hostname; + const k8sPort = url.port ? Number(url.port) : 443; + const k8sHost = `${url.protocol}//${url.hostname}`; - const httpsAgent = - providerInputs.ca && providerInputs.sslEnabled - ? new https.Agent({ - ca: providerInputs.ca, - rejectUnauthorized: true - }) - : undefined; + const httpsAgent = + providerInputs.ca && providerInputs.sslEnabled + ? new https.Agent({ + ca: providerInputs.ca, + rejectUnauthorized: true + }) + : undefined; - if (providerInputs.gatewayId) { - if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) { - await $gatewayProxyWrapper( - { - gatewayId: providerInputs.gatewayId, - targetHost: k8sHost, - targetPort: k8sPort, - httpsAgent, - reviewTokenThroughGateway: true - }, - serviceAccountDynamicCallback - ); + if (providerInputs.gatewayId) { + if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) { + await $gatewayProxyWrapper( + { + gatewayId: providerInputs.gatewayId, + targetHost: k8sHost, + targetPort: k8sPort, + httpsAgent, + reviewTokenThroughGateway: true + }, + serviceAccountDynamicCallback + ); + } else { + await $gatewayProxyWrapper( + { + gatewayId: providerInputs.gatewayId, + targetHost: k8sGatewayHost, + targetPort: k8sPort, + httpsAgent, + reviewTokenThroughGateway: false + }, + serviceAccountDynamicCallback + ); + } } else { - await $gatewayProxyWrapper( - { - gatewayId: providerInputs.gatewayId, - targetHost: k8sGatewayHost, - targetPort: k8sPort, - httpsAgent, - reviewTokenThroughGateway: false - }, - serviceAccountDynamicCallback - ); + await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent); } - } else { - await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent); + } catch (error) { + let errorMessage = error instanceof Error ? error.message : "Unknown error"; + if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) { + errorMessage = (error.response?.data as { message: string }).message; + } + + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: errorMessage, + tokens: [entityId, providerInputs.clusterToken || ""] + }); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` + }); } } diff --git a/backend/src/ee/services/dynamic-secret/providers/ldap.ts b/backend/src/ee/services/dynamic-secret/providers/ldap.ts index 1de8aa1e6..10e0b88f4 100644 --- a/backend/src/ee/services/dynamic-secret/providers/ldap.ts +++ b/backend/src/ee/services/dynamic-secret/providers/ldap.ts @@ -6,6 +6,7 @@ import RE2 from "re2"; import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { LdapCredentialType, LdapSchema, TDynamicProviderFns } from "./models"; @@ -91,8 +92,18 @@ export const LdapProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await $getClient(providerInputs); - return client.connected; + try { + const client = await $getClient(providerInputs); + return client.connected; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.bindpass, providerInputs.binddn] + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; const executeLdif = async (client: ldapjs.Client, ldif_file: string) => { @@ -205,11 +216,11 @@ export const LdapProvider = (): TDynamicProviderFns => { if (providerInputs.credentialType === LdapCredentialType.Static) { const dnRegex = new RE2("^dn:\\s*(.+)", "m"); const dnMatch = dnRegex.exec(providerInputs.rotationLdif); + const username = dnMatch?.[1]; + if (!username) throw new BadRequestError({ message: "Username not found from Ldif" }); + const password = generatePassword(); if (dnMatch) { - const username = dnMatch[1]; - const password = generatePassword(); - const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rotationLdif }); try { @@ -217,7 +228,11 @@ export const LdapProvider = (): TDynamicProviderFns => { return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } }; } catch (err) { - throw new BadRequestError({ message: (err as Error).message }); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, password, providerInputs.binddn, providerInputs.bindpass] + }); + throw new BadRequestError({ message: sanitizedErrorMessage }); } } else { throw new BadRequestError({ @@ -238,7 +253,11 @@ export const LdapProvider = (): TDynamicProviderFns => { const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif }); await executeLdif(client, rollbackLdif); } - throw new BadRequestError({ message: (err as Error).message }); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, password, providerInputs.binddn, providerInputs.bindpass] + }); + throw new BadRequestError({ message: sanitizedErrorMessage }); } } }; @@ -262,7 +281,11 @@ export const LdapProvider = (): TDynamicProviderFns => { return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } }; } catch (err) { - throw new BadRequestError({ message: (err as Error).message }); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, password, providerInputs.binddn, providerInputs.bindpass] + }); + throw new BadRequestError({ message: sanitizedErrorMessage }); } } else { throw new BadRequestError({ @@ -278,7 +301,7 @@ export const LdapProvider = (): TDynamicProviderFns => { return { entityId }; }; - const renew = async (inputs: unknown, entityId: string) => { + const renew = async (_inputs: unknown, entityId: string) => { // No renewal necessary return { entityId }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts b/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts index 6da8b4b4e..7f19c2d3e 100644 --- a/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts +++ b/backend/src/ee/services/dynamic-secret/providers/mongo-atlas.ts @@ -3,6 +3,8 @@ import { customAlphabet } from "nanoid"; import { z } from "zod"; import { createDigestAuthRequestInterceptor } from "@app/lib/axios/digest-auth"; +import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models"; @@ -49,19 +51,25 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { const providerInputs = await validateProviderInputs(inputs); const client = await $getClient(providerInputs); - const isConnected = await client({ - method: "GET", - url: `v2/groups/${providerInputs.groupId}/databaseUsers`, - params: { itemsPerPage: 1 } - }) - .then(() => true) - .catch((error) => { - if ((error as AxiosError).response) { - throw new Error(JSON.stringify((error as AxiosError).response?.data)); - } - throw error; + try { + const isConnected = await client({ + method: "GET", + url: `v2/groups/${providerInputs.groupId}/databaseUsers`, + params: { itemsPerPage: 1 } + }).then(() => true); + return isConnected; + } catch (error) { + const errorMessage = (error as AxiosError).response + ? JSON.stringify((error as AxiosError).response?.data) + : (error as Error)?.message; + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: errorMessage, + tokens: [providerInputs.adminPublicKey, providerInputs.adminPrivateKey, providerInputs.groupId] }); - return isConnected; + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; const create = async (data: { @@ -77,25 +85,39 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); const expiration = new Date(expireAt).toISOString(); - await client({ - method: "POST", - url: `/v2/groups/${providerInputs.groupId}/databaseUsers`, - data: { - roles: providerInputs.roles, - scopes: providerInputs.scopes, - deleteAfterDate: expiration, - username, - password, - databaseName: "admin", - groupId: providerInputs.groupId - } - }).catch((error) => { - if ((error as AxiosError).response) { - throw new Error(JSON.stringify((error as AxiosError).response?.data)); - } - throw error; - }); - return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + try { + await client({ + method: "POST", + url: `/v2/groups/${providerInputs.groupId}/databaseUsers`, + data: { + roles: providerInputs.roles, + scopes: providerInputs.scopes, + deleteAfterDate: expiration, + username, + password, + databaseName: "admin", + groupId: providerInputs.groupId + } + }); + return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + } catch (error) { + const errorMessage = (error as AxiosError).response + ? JSON.stringify((error as AxiosError).response?.data) + : (error as Error)?.message; + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: errorMessage, + tokens: [ + username, + password, + providerInputs.adminPublicKey, + providerInputs.adminPrivateKey, + providerInputs.groupId + ] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); + } }; const revoke = async (inputs: unknown, entityId: string) => { @@ -111,15 +133,23 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { throw err; }); if (isExisting) { - await client({ - method: "DELETE", - url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}` - }).catch((error) => { - if ((error as AxiosError).response) { - throw new Error(JSON.stringify((error as AxiosError).response?.data)); - } - throw error; - }); + try { + await client({ + method: "DELETE", + url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}` + }); + } catch (error) { + const errorMessage = (error as AxiosError).response + ? JSON.stringify((error as AxiosError).response?.data) + : (error as Error)?.message; + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: errorMessage, + tokens: [username, providerInputs.adminPublicKey, providerInputs.adminPrivateKey, providerInputs.groupId] + }); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` + }); + } } return { entityId: username }; @@ -132,21 +162,29 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => { const username = entityId; const expiration = new Date(expireAt).toISOString(); - await client({ - method: "PATCH", - url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`, - data: { - deleteAfterDate: expiration, - databaseName: "admin", - groupId: providerInputs.groupId - } - }).catch((error) => { - if ((error as AxiosError).response) { - throw new Error(JSON.stringify((error as AxiosError).response?.data)); - } - throw error; - }); - return { entityId: username }; + try { + await client({ + method: "PATCH", + url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`, + data: { + deleteAfterDate: expiration, + databaseName: "admin", + groupId: providerInputs.groupId + } + }); + return { entityId: username }; + } catch (error) { + const errorMessage = (error as AxiosError).response + ? JSON.stringify((error as AxiosError).response?.data) + : (error as Error)?.message; + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: errorMessage, + tokens: [username, providerInputs.adminPublicKey, providerInputs.adminPrivateKey, providerInputs.groupId] + }); + throw new BadRequestError({ + message: `Failed to renew lease from provider: ${sanitizedErrorMessage}` + }); + } }; return { diff --git a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts index 331a355a7..dfae417f6 100644 --- a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts +++ b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts @@ -2,6 +2,8 @@ import { MongoClient } from "mongodb"; import { customAlphabet } from "nanoid"; import { z } from "zod"; +import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; @@ -51,13 +53,24 @@ export const MongoDBProvider = (): TDynamicProviderFns => { const providerInputs = await validateProviderInputs(inputs); const client = await $getClient(providerInputs); - const isConnected = await client - .db(providerInputs.database) - .command({ ping: 1 }) - .then(() => true); + try { + const isConnected = await client + .db(providerInputs.database) + .command({ ping: 1 }) + .then(() => true); - await client.close(); - return isConnected; + await client.close(); + return isConnected; + } catch (err) { + await client.close(); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.password, providerInputs.username, providerInputs.database, providerInputs.host] + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => { @@ -68,16 +81,27 @@ export const MongoDBProvider = (): TDynamicProviderFns => { const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); - const db = client.db(providerInputs.database); + try { + const db = client.db(providerInputs.database); - await db.command({ - createUser: username, - pwd: password, - roles: providerInputs.roles - }); - await client.close(); + await db.command({ + createUser: username, + pwd: password, + roles: providerInputs.roles + }); + await client.close(); - return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + } catch (err) { + await client.close(); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, password, providerInputs.password, providerInputs.username, providerInputs.database] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); + } }; const revoke = async (inputs: unknown, entityId: string) => { @@ -86,13 +110,24 @@ export const MongoDBProvider = (): TDynamicProviderFns => { const username = entityId; - const db = client.db(providerInputs.database); - await db.command({ - dropUser: username - }); - await client.close(); + try { + const db = client.db(providerInputs.database); + await db.command({ + dropUser: username + }); + await client.close(); - return { entityId: username }; + return { entityId: username }; + } catch (err) { + await client.close(); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, providerInputs.password, providerInputs.username, providerInputs.database] + }); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` + }); + } }; const renew = async (_inputs: unknown, entityId: string) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts index 76081c86c..2660d9d8a 100644 --- a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts +++ b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts @@ -3,6 +3,8 @@ import https from "https"; import { customAlphabet } from "nanoid"; import { z } from "zod"; +import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -110,11 +112,19 @@ export const RabbitMqProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await $getClient(providerInputs); - - const infoResponse = await connection.get("/whoami").then(() => true); - - return infoResponse; + try { + const connection = await $getClient(providerInputs); + const infoResponse = await connection.get("/whoami").then(() => true); + return infoResponse; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.password, providerInputs.username, providerInputs.host] + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => { @@ -125,26 +135,44 @@ export const RabbitMqProvider = (): TDynamicProviderFns => { const username = generateUsername(usernameTemplate, identity); const password = generatePassword(); - await createRabbitMqUser({ - axiosInstance: connection, - virtualHost: providerInputs.virtualHost, - createUser: { - password, - username, - tags: [...(providerInputs.tags ?? []), "infisical-user"] - } - }); - - return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + try { + await createRabbitMqUser({ + axiosInstance: connection, + virtualHost: providerInputs.virtualHost, + createUser: { + password, + username, + tags: [...(providerInputs.tags ?? []), "infisical-user"] + } + }); + return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, password, providerInputs.password, providerInputs.username] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); + } }; const revoke = async (inputs: unknown, entityId: string) => { const providerInputs = await validateProviderInputs(inputs); const connection = await $getClient(providerInputs); - await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId }); - - return { entityId }; + try { + await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId }); + return { entityId }; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [entityId, providerInputs.password, providerInputs.username] + }); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` + }); + } }; const renew = async (_inputs: unknown, entityId: string) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/redis.ts b/backend/src/ee/services/dynamic-secret/providers/redis.ts index 989ed96dc..621c7d761 100644 --- a/backend/src/ee/services/dynamic-secret/providers/redis.ts +++ b/backend/src/ee/services/dynamic-secret/providers/redis.ts @@ -4,6 +4,7 @@ import { customAlphabet } from "nanoid"; import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -112,14 +113,27 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const connection = await $getClient(providerInputs); - - const pingResponse = await connection - .ping() - .then(() => true) - .catch(() => false); - - return pingResponse; + let connection; + try { + connection = await $getClient(providerInputs); + const pingResponse = await connection.ping().then(() => true); + await connection.quit(); + return pingResponse; + } catch (err) { + if (connection) await connection.quit(); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [ + providerInputs.password || "", + providerInputs.username, + providerInputs.host, + String(providerInputs.port) + ] + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; const create = async (data: { @@ -144,10 +158,20 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { const queries = creationStatement.toString().split(";").filter(Boolean); - await executeTransactions(connection, queries); - - await connection.quit(); - return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + try { + await executeTransactions(connection, queries); + await connection.quit(); + return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + } catch (err) { + await connection.quit(); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, password, providerInputs.password || "", providerInputs.username] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); + } }; const revoke = async (inputs: unknown, entityId: string) => { @@ -159,10 +183,20 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username }); const queries = revokeStatement.toString().split(";").filter(Boolean); - await executeTransactions(connection, queries); - - await connection.quit(); - return { entityId: username }; + try { + await executeTransactions(connection, queries); + await connection.quit(); + return { entityId: username }; + } catch (err) { + await connection.quit(); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, providerInputs.password || "", providerInputs.username] + }); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` + }); + } }; const renew = async (inputs: unknown, entityId: string, expireAt: number) => { @@ -176,13 +210,23 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => { const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration }); - if (renewStatement) { - const queries = renewStatement.toString().split(";").filter(Boolean); - await executeTransactions(connection, queries); + try { + if (renewStatement) { + const queries = renewStatement.toString().split(";").filter(Boolean); + await executeTransactions(connection, queries); + } + await connection.quit(); + return { entityId: username }; + } catch (err) { + await connection.quit(); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, providerInputs.password || "", providerInputs.username] + }); + throw new BadRequestError({ + message: `Failed to renew lease from provider: ${sanitizedErrorMessage}` + }); } - - await connection.quit(); - return { entityId: username }; }; return { diff --git a/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts index 9c13d3efc..b84859484 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sap-ase.ts @@ -4,6 +4,7 @@ import odbc from "odbc"; import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -67,25 +68,41 @@ export const SapAseProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const masterClient = await $getClient(providerInputs, true); - const client = await $getClient(providerInputs); + let masterClient; + let client; + try { + masterClient = await $getClient(providerInputs, true); + client = await $getClient(providerInputs); - const [resultFromMasterDatabase] = await masterClient.query<{ version: string }>("SELECT @@VERSION AS version"); - const [resultFromSelectedDatabase] = await client.query<{ version: string }>("SELECT @@VERSION AS version"); + const [resultFromMasterDatabase] = await masterClient.query<{ version: string }>("SELECT @@VERSION AS version"); + const [resultFromSelectedDatabase] = await client.query<{ version: string }>("SELECT @@VERSION AS version"); - if (!resultFromSelectedDatabase.version) { + if (!resultFromSelectedDatabase.version) { + throw new BadRequestError({ + message: "Failed to validate SAP ASE connection, version query failed" + }); + } + + if (resultFromMasterDatabase.version !== resultFromSelectedDatabase.version) { + throw new BadRequestError({ + message: "Failed to validate SAP ASE connection (master), version mismatch" + }); + } + + await masterClient.close(); + await client.close(); + return true; + } catch (err) { + if (masterClient) await masterClient.close(); + if (client) await client.close(); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.password, providerInputs.username, providerInputs.host, providerInputs.database] + }); throw new BadRequestError({ - message: "Failed to validate SAP ASE connection, version query failed" + message: `Failed to connect with provider: ${sanitizedErrorMessage}` }); } - - if (resultFromMasterDatabase.version !== resultFromSelectedDatabase.version) { - throw new BadRequestError({ - message: "Failed to validate SAP ASE connection (master), version mismatch" - }); - } - - return true; }; const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => { @@ -105,16 +122,26 @@ export const SapAseProvider = (): TDynamicProviderFns => { const queries = creationStatement.trim().replaceAll("\n", "").split(";").filter(Boolean); - for await (const query of queries) { - // If it's an adduser query, we need to first call sp_addlogin on the MASTER database. - // If not done, then the newly created user won't be able to authenticate. - await (query.startsWith(SapCommands.CreateLogin) ? masterClient : client).query(query); + try { + for await (const query of queries) { + // If it's an adduser query, we need to first call sp_addlogin on the MASTER database. + // If not done, then the newly created user won't be able to authenticate. + await (query.startsWith(SapCommands.CreateLogin) ? masterClient : client).query(query); + } + await masterClient.close(); + await client.close(); + return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + } catch (err) { + await masterClient.close(); + await client.close(); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, password, providerInputs.password, providerInputs.username, providerInputs.database] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); } - - await masterClient.close(); - await client.close(); - - return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; }; const revoke = async (inputs: unknown, username: string) => { @@ -140,14 +167,24 @@ export const SapAseProvider = (): TDynamicProviderFns => { } } - for await (const query of queries) { - await (query.startsWith(SapCommands.DropLogin) ? masterClient : client).query(query); + try { + for await (const query of queries) { + await (query.startsWith(SapCommands.DropLogin) ? masterClient : client).query(query); + } + await masterClient.close(); + await client.close(); + return { entityId: username }; + } catch (err) { + await masterClient.close(); + await client.close(); + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, providerInputs.password, providerInputs.username, providerInputs.database] + }); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` + }); } - - await masterClient.close(); - await client.close(); - - return { entityId: username }; }; const renew = async (_: unknown, username: string) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts index 5c8a75555..53b88a192 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sap-hana.ts @@ -10,6 +10,7 @@ import { customAlphabet } from "nanoid"; import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -83,19 +84,26 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await $getClient(providerInputs); - - const testResult = await new Promise((resolve, reject) => { - client.exec("SELECT 1 FROM DUMMY;", (err: any) => { - if (err) { - reject(); - } - - resolve(true); + try { + const client = await $getClient(providerInputs); + const testResult = await new Promise((resolve, reject) => { + client.exec("SELECT 1 FROM DUMMY;", (err: any) => { + if (err) { + return reject(err); + } + resolve(true); + }); }); - }); - - return testResult; + return testResult; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.password, providerInputs.username, providerInputs.host] + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; const create = async (data: { @@ -119,18 +127,22 @@ export const SapHanaProvider = (): TDynamicProviderFns => { }); const queries = creationStatement.toString().split(";").filter(Boolean); - for await (const query of queries) { - await new Promise((resolve, reject) => { - client.exec(query, (err: any) => { - if (err) { - reject( - new BadRequestError({ - message: err.message - }) - ); - } - resolve(true); + try { + for await (const query of queries) { + await new Promise((resolve, reject) => { + client.exec(query, (err: any) => { + if (err) return reject(err); + resolve(true); + }); }); + } + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, password, providerInputs.password, providerInputs.username] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` }); } @@ -142,18 +154,24 @@ export const SapHanaProvider = (): TDynamicProviderFns => { const client = await $getClient(providerInputs); const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username }); const queries = revokeStatement.toString().split(";").filter(Boolean); - for await (const query of queries) { - await new Promise((resolve, reject) => { - client.exec(query, (err: any) => { - if (err) { - reject( - new BadRequestError({ - message: err.message - }) - ); - } - resolve(true); + try { + for await (const query of queries) { + await new Promise((resolve, reject) => { + client.exec(query, (err: any) => { + if (err) { + reject(err); + } + resolve(true); + }); }); + } + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, providerInputs.password, providerInputs.username] + }); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` }); } @@ -174,16 +192,20 @@ export const SapHanaProvider = (): TDynamicProviderFns => { await new Promise((resolve, reject) => { client.exec(query, (err: any) => { if (err) { - reject( - new BadRequestError({ - message: err.message - }) - ); + reject(err); } resolve(true); }); }); } + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [entityId, providerInputs.password, providerInputs.username] + }); + throw new BadRequestError({ + message: `Failed to renew lease from provider: ${sanitizedErrorMessage}` + }); } finally { client.disconnect(); } diff --git a/backend/src/ee/services/dynamic-secret/providers/snowflake.ts b/backend/src/ee/services/dynamic-secret/providers/snowflake.ts index 9e97ecd30..a335c427b 100644 --- a/backend/src/ee/services/dynamic-secret/providers/snowflake.ts +++ b/backend/src/ee/services/dynamic-secret/providers/snowflake.ts @@ -4,6 +4,7 @@ import snowflake from "snowflake-sdk"; import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -69,12 +70,10 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { const validateConnection = async (inputs: unknown) => { const providerInputs = await validateProviderInputs(inputs); - const client = await $getClient(providerInputs); - - let isValidConnection: boolean; - + let client; try { - isValidConnection = await Promise.race([ + client = await $getClient(providerInputs); + const isValidConnection = await Promise.race([ client.isValidAsync(), new Promise((resolve) => { setTimeout(resolve, 10000); @@ -82,11 +81,18 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { throw new BadRequestError({ message: "Unable to establish connection - verify credentials" }); }) ]); + return isValidConnection; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.password, providerInputs.username, providerInputs.accountId, providerInputs.orgId] + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); } finally { - client.destroy(noop); + if (client) client.destroy(noop); } - - return isValidConnection; }; const create = async (data: { @@ -116,13 +122,19 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { sqlText: creationStatement, complete(err) { if (err) { - return reject(new BadRequestError({ name: "CreateLease", message: err.message })); + return reject(err); } return resolve(true); } }); }); + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error).message, + tokens: [username, password, providerInputs.password, providerInputs.username] + }); + throw new BadRequestError({ message: `Failed to create lease from provider: ${sanitizedErrorMessage}` }); } finally { client.destroy(noop); } @@ -143,13 +155,19 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { sqlText: revokeStatement, complete(err) { if (err) { - return reject(new BadRequestError({ name: "RevokeLease", message: err.message })); + return reject(err); } return resolve(true); } }); }); + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error).message, + tokens: [username, providerInputs.password, providerInputs.username] + }); + throw new BadRequestError({ message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` }); } finally { client.destroy(noop); } @@ -175,13 +193,19 @@ export const SnowflakeProvider = (): TDynamicProviderFns => { sqlText: renewStatement, complete(err) { if (err) { - return reject(new BadRequestError({ name: "RenewLease", message: err.message })); + return reject(err); } return resolve(true); } }); }); + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error).message, + tokens: [entityId, providerInputs.password, providerInputs.username] + }); + throw new BadRequestError({ message: `Failed to renew lease from provider: ${sanitizedErrorMessage}` }); } finally { client.destroy(noop); } diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index c86e3aff5..c8d036ce3 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -3,6 +3,8 @@ import knex from "knex"; import { z } from "zod"; import { crypto } from "@app/lib/crypto/cryptography"; +import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; @@ -212,8 +214,19 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) // oracle needs from keyword const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1"; - isConnected = await db.raw(testStatement).then(() => true); - await db.destroy(); + try { + isConnected = await db.raw(testStatement).then(() => true); + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [providerInputs.username] + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } finally { + await db.destroy(); + } }; if (providerInputs.gatewayId) { @@ -233,13 +246,13 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) const { inputs, expireAt, usernameTemplate, identity } = data; const providerInputs = await validateProviderInputs(inputs); + const { database } = providerInputs; const username = generateUsername(providerInputs.client, usernameTemplate, identity); const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements); const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { const db = await $getClient({ ...providerInputs, port, host }); try { - const { database } = providerInputs; const expiration = new Date(expireAt).toISOString(); const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ @@ -256,6 +269,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) await tx.raw(query); } }); + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, password, database] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); } finally { await db.destroy(); } @@ -283,6 +304,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) await tx.raw(query); } }); + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, database] + }); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` + }); } finally { await db.destroy(); } @@ -319,6 +348,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) } }); } + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [database] + }); + throw new BadRequestError({ + message: `Failed to renew lease from provider: ${sanitizedErrorMessage}` + }); } finally { await db.destroy(); } diff --git a/backend/src/ee/services/dynamic-secret/providers/totp.ts b/backend/src/ee/services/dynamic-secret/providers/totp.ts index d16b82306..4e1b504f0 100644 --- a/backend/src/ee/services/dynamic-secret/providers/totp.ts +++ b/backend/src/ee/services/dynamic-secret/providers/totp.ts @@ -1,6 +1,8 @@ import { authenticator } from "otplib"; import { HashAlgorithms } from "otplib/core"; +import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models"; @@ -12,62 +14,84 @@ export const TotpProvider = (): TDynamicProviderFns => { return providerInputs; }; - const validateConnection = async () => { - return true; + const validateConnection = async (inputs: unknown) => { + try { + await validateProviderInputs(inputs); + return true; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [] + }); + throw new BadRequestError({ + message: `Failed to connect with provider: ${sanitizedErrorMessage}` + }); + } }; - const create = async (inputs: unknown) => { - const providerInputs = await validateProviderInputs(inputs); + const create = async (data: { inputs: unknown }) => { + const { inputs } = data; + try { + const providerInputs = await validateProviderInputs(inputs); - const entityId = alphaNumericNanoId(32); - const authenticatorInstance = authenticator.clone(); + const entityId = alphaNumericNanoId(32); + const authenticatorInstance = authenticator.clone(); - let secret: string; - let period: number | null | undefined; - let digits: number | null | undefined; - let algorithm: HashAlgorithms | null | undefined; + let secret: string; + let period: number | null | undefined; + let digits: number | null | undefined; + let algorithm: HashAlgorithms | null | undefined; - if (providerInputs.configType === TotpConfigType.URL) { - const urlObj = new URL(providerInputs.url); - secret = urlObj.searchParams.get("secret") as string; - const periodFromUrl = urlObj.searchParams.get("period"); - const digitsFromUrl = urlObj.searchParams.get("digits"); - const algorithmFromUrl = urlObj.searchParams.get("algorithm"); + if (providerInputs.configType === TotpConfigType.URL) { + const urlObj = new URL(providerInputs.url); + secret = urlObj.searchParams.get("secret") as string; + const periodFromUrl = urlObj.searchParams.get("period"); + const digitsFromUrl = urlObj.searchParams.get("digits"); + const algorithmFromUrl = urlObj.searchParams.get("algorithm"); - if (periodFromUrl) { - period = +periodFromUrl; + if (periodFromUrl) { + period = +periodFromUrl; + } + + if (digitsFromUrl) { + digits = +digitsFromUrl; + } + + if (algorithmFromUrl) { + algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms; + } + } else { + secret = providerInputs.secret; + period = providerInputs.period; + digits = providerInputs.digits; + algorithm = providerInputs.algorithm as unknown as HashAlgorithms; } - if (digitsFromUrl) { - digits = +digitsFromUrl; + if (digits) { + authenticatorInstance.options = { digits }; } - if (algorithmFromUrl) { - algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms; + if (algorithm) { + authenticatorInstance.options = { algorithm }; } - } else { - secret = providerInputs.secret; - period = providerInputs.period; - digits = providerInputs.digits; - algorithm = providerInputs.algorithm as unknown as HashAlgorithms; - } - if (digits) { - authenticatorInstance.options = { digits }; - } + if (period) { + authenticatorInstance.options = { step: period }; + } - if (algorithm) { - authenticatorInstance.options = { algorithm }; + return { + entityId, + data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() } + }; + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); } - - if (period) { - authenticatorInstance.options = { step: period }; - } - - return { - entityId, - data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() } - }; }; const revoke = async (_inputs: unknown, entityId: string) => { diff --git a/backend/src/ee/services/dynamic-secret/providers/vertica.ts b/backend/src/ee/services/dynamic-secret/providers/vertica.ts index 0e60cddb3..8fa332bd5 100644 --- a/backend/src/ee/services/dynamic-secret/providers/vertica.ts +++ b/backend/src/ee/services/dynamic-secret/providers/vertica.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; +import { sanitizeString } from "@app/lib/fn"; import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -275,6 +276,14 @@ export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynam await client.raw(trimmedQuery); } } + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, password, providerInputs.username, providerInputs.password] + }); + throw new BadRequestError({ + message: `Failed to create lease from provider: ${sanitizedErrorMessage}` + }); } finally { if (client) await client.destroy(); } @@ -339,6 +348,14 @@ export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynam await client.raw(trimmedQuery); } } + } catch (err) { + const sanitizedErrorMessage = sanitizeString({ + unsanitizedString: (err as Error)?.message, + tokens: [username, providerInputs.username, providerInputs.password] + }); + throw new BadRequestError({ + message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` + }); } finally { if (client) await client.destroy(); } diff --git a/backend/src/lib/fn/string.ts b/backend/src/lib/fn/string.ts index 2fa4c9166..61e5a7e20 100644 --- a/backend/src/lib/fn/string.ts +++ b/backend/src/lib/fn/string.ts @@ -19,3 +19,17 @@ export const prefixWithSlash = (str: string) => { const vowelRegex = new RE2(/^[aeiou]/i); export const startsWithVowel = (str: string) => vowelRegex.test(str); + +const pickWordsRegex = new RE2(/(\W+)/); +export const sanitizeString = (dto: { unsanitizedString: string; tokens: string[] }) => { + const words = dto.unsanitizedString.split(pickWordsRegex); + + const redactionSet = new Set(dto.tokens.filter(Boolean)); + const sanitizedWords = words.map((el) => { + if (redactionSet.has(el)) { + return "[REDACTED]"; + } + return el; + }); + return sanitizedWords.join(""); +};