Merge pull request #4889 from Infisical/feature/mongodb-secret-rotation

feature(secret-rotation): add mongodb app connection and secret rotation
This commit is contained in:
Victor Hugo dos Santos
2025-12-08 18:48:43 -03:00
committed by GitHub
64 changed files with 1447 additions and 26 deletions

View File

@@ -4,6 +4,7 @@ import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-r
import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router";
import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-rotation-router";
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
import { registerMongoDBCredentialsRotationRouter } from "./mongodb-credentials-rotation-router";
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rotation-router";
import { registerOktaClientSecretRotationRouter } from "./okta-client-secret-rotation-router";
@@ -26,5 +27,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
[SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter,
[SecretRotation.RedisCredentials]: registerRedisCredentialsRotationRouter
[SecretRotation.RedisCredentials]: registerRedisCredentialsRotationRouter,
[SecretRotation.MongoDBCredentials]: registerMongoDBCredentialsRotationRouter
};

View File

@@ -0,0 +1,19 @@
import {
CreateMongoDBCredentialsRotationSchema,
MongoDBCredentialsRotationGeneratedCredentialsSchema,
MongoDBCredentialsRotationSchema,
UpdateMongoDBCredentialsRotationSchema
} from "@app/ee/services/secret-rotation-v2/mongodb-credentials";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
export const registerMongoDBCredentialsRotationRouter = async (server: FastifyZodProvider) =>
registerSecretRotationEndpoints({
type: SecretRotation.MongoDBCredentials,
server,
responseSchema: MongoDBCredentialsRotationSchema,
createSchema: CreateMongoDBCredentialsRotationSchema,
updateSchema: UpdateMongoDBCredentialsRotationSchema,
generatedCredentialsSchema: MongoDBCredentialsRotationGeneratedCredentialsSchema
});

View File

@@ -5,6 +5,7 @@ import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret
import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret";
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MongoDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mongodb-credentials";
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials";
import { OktaClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
@@ -27,7 +28,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
AwsIamUserSecretRotationListItemSchema,
LdapPasswordRotationListItemSchema,
OktaClientSecretRotationListItemSchema,
RedisCredentialsRotationListItemSchema
RedisCredentialsRotationListItemSchema,
MongoDBCredentialsRotationListItemSchema
]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,4 @@
export * from "./mongodb-credentials-rotation-constants";
export * from "./mongodb-credentials-rotation-fns";
export * from "./mongodb-credentials-rotation-schemas";
export * from "./mongodb-credentials-rotation-types";

View File

@@ -0,0 +1,27 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const MONGODB_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
name: "MongoDB Credentials",
type: SecretRotation.MongoDBCredentials,
connection: AppConnection.MongoDB,
template: {
createUserStatement: `use [DATABASE_NAME]
db.createUser({
user: "infisical_user_1",
pwd: "temporary_password",
roles: [{ role: "readWrite", db: "[DATABASE_NAME]" }]
})
db.createUser({
user: "infisical_user_2",
pwd: "temporary_password",
roles: [{ role: "readWrite", db: "[DATABASE_NAME]" }]
})`,
secretsMapping: {
username: "MONGODB_DB_USERNAME",
password: "MONGODB_DB_PASSWORD"
}
}
};

View File

@@ -0,0 +1,191 @@
/* eslint-disable no-await-in-loop */
import { MongoClient } from "mongodb";
import {
TRotationFactory,
TRotationFactoryGetSecretsPayload,
TRotationFactoryIssueCredentials,
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { createMongoClient } from "@app/services/app-connection/mongodb/mongodb-connection-fns";
import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../shared/utils";
import {
TMongoDBCredentialsRotationGeneratedCredentials,
TMongoDBCredentialsRotationWithConnection
} from "./mongodb-credentials-rotation-types";
const redactPasswords = (e: unknown, credentials: TMongoDBCredentialsRotationGeneratedCredentials) => {
const error = e as Error;
if (!error?.message) return "Unknown error";
let redactedMessage = error.message;
credentials.forEach(({ password }) => {
redactedMessage = redactedMessage.replaceAll(password, "*******************");
});
return redactedMessage;
};
export const mongodbCredentialsRotationFactory: TRotationFactory<
TMongoDBCredentialsRotationWithConnection,
TMongoDBCredentialsRotationGeneratedCredentials
> = (secretRotation) => {
const {
connection,
parameters: { username1, username2 },
activeIndex,
secretsMapping
} = secretRotation;
const passwordRequirement = DEFAULT_PASSWORD_REQUIREMENTS;
const $getClient = async () => {
let client: MongoClient | null = null;
try {
client = await createMongoClient(connection.credentials, { validateConnection: true });
return client;
} catch (err) {
if (client) await client.close();
throw err;
}
};
const $validateCredentials = async (credentials: TMongoDBCredentialsRotationGeneratedCredentials[number]) => {
let client: MongoClient | null = null;
try {
client = await createMongoClient(connection.credentials, {
authCredentials: {
username: credentials.username,
password: credentials.password
},
validateConnection: true
});
} catch (error) {
throw new Error(redactPasswords(error, [credentials]));
} finally {
if (client) await client.close();
}
};
const issueCredentials: TRotationFactoryIssueCredentials<TMongoDBCredentialsRotationGeneratedCredentials> = async (
callback
) => {
// For MongoDB, since we get existing users, we change both their passwords
// on issue to invalidate their existing passwords
const credentialsSet = [
{ username: username1, password: generatePassword(passwordRequirement) },
{ username: username2, password: generatePassword(passwordRequirement) }
];
let client: MongoClient | null = null;
try {
client = await $getClient();
const db = client.db(connection.credentials.database);
for (const credentials of credentialsSet) {
await db.command({
updateUser: credentials.username,
pwd: credentials.password
});
}
} catch (error) {
throw new Error(redactPasswords(error, credentialsSet));
} finally {
if (client) await client.close();
}
for (const credentials of credentialsSet) {
await $validateCredentials(credentials);
}
return callback(credentialsSet[0]);
};
const revokeCredentials: TRotationFactoryRevokeCredentials<TMongoDBCredentialsRotationGeneratedCredentials> = async (
credentialsToRevoke,
callback
) => {
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({
username,
password: generatePassword(passwordRequirement)
}));
let client: MongoClient | null = null;
try {
client = await $getClient();
const db = client.db(connection.credentials.database);
for (const credentials of revokedCredentials) {
await db.command({
updateUser: credentials.username,
pwd: credentials.password
});
}
} catch (error) {
throw new Error(redactPasswords(error, revokedCredentials));
} finally {
if (client) await client.close();
}
return callback();
};
const rotateCredentials: TRotationFactoryRotateCredentials<TMongoDBCredentialsRotationGeneratedCredentials> = async (
_,
callback
) => {
const credentials = {
username: activeIndex === 0 ? username2 : username1,
password: generatePassword(passwordRequirement)
};
let client: MongoClient | null = null;
try {
client = await $getClient();
const db = client.db(connection.credentials.database);
await db.command({
updateUser: credentials.username,
pwd: credentials.password
});
} catch (error) {
throw new Error(redactPasswords(error, [credentials]));
} finally {
if (client) await client.close();
}
await $validateCredentials(credentials);
return callback(credentials);
};
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TMongoDBCredentialsRotationGeneratedCredentials> = (
generatedCredentials
) => {
const { username, password } = secretsMapping;
const secrets = [
{
key: username,
value: generatedCredentials.username
},
{
key: password,
value: generatedCredentials.password
}
];
return secrets;
};
return {
issueCredentials,
revokeCredentials,
rotateCredentials,
getSecretsPayload
};
};

View File

@@ -0,0 +1,52 @@
import { z } from "zod";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
BaseCreateSecretRotationSchema,
BaseSecretRotationSchema,
BaseUpdateSecretRotationSchema
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
import {
SqlCredentialsRotationGeneratedCredentialsSchema,
SqlCredentialsRotationParametersSchema,
SqlCredentialsRotationTemplateSchema
} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-schemas";
import { SecretRotations } from "@app/lib/api-docs";
import { SecretNameSchema } from "@app/server/lib/schemas";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const MongoDBCredentialsRotationGeneratedCredentialsSchema = SqlCredentialsRotationGeneratedCredentialsSchema;
export const MongoDBCredentialsRotationParametersSchema = SqlCredentialsRotationParametersSchema;
export const MongoDBCredentialsRotationTemplateSchema = SqlCredentialsRotationTemplateSchema;
const MongoDBCredentialsRotationSecretsMappingSchema = z.object({
username: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.MONGODB_CREDENTIALS.username),
password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.MONGODB_CREDENTIALS.password)
});
export const MongoDBCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.MongoDBCredentials).extend({
type: z.literal(SecretRotation.MongoDBCredentials),
parameters: MongoDBCredentialsRotationParametersSchema,
secretsMapping: MongoDBCredentialsRotationSecretsMappingSchema
});
export const CreateMongoDBCredentialsRotationSchema = BaseCreateSecretRotationSchema(
SecretRotation.MongoDBCredentials
).extend({
parameters: MongoDBCredentialsRotationParametersSchema,
secretsMapping: MongoDBCredentialsRotationSecretsMappingSchema
});
export const UpdateMongoDBCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
SecretRotation.MongoDBCredentials
).extend({
parameters: MongoDBCredentialsRotationParametersSchema.optional(),
secretsMapping: MongoDBCredentialsRotationSecretsMappingSchema.optional()
});
export const MongoDBCredentialsRotationListItemSchema = z.object({
name: z.literal("MongoDB Credentials"),
connection: z.literal(AppConnection.MongoDB),
type: z.literal(SecretRotation.MongoDBCredentials),
template: MongoDBCredentialsRotationTemplateSchema
});

View File

@@ -0,0 +1,24 @@
import { z } from "zod";
import { TMongoDBConnection } from "@app/services/app-connection/mongodb";
import {
CreateMongoDBCredentialsRotationSchema,
MongoDBCredentialsRotationGeneratedCredentialsSchema,
MongoDBCredentialsRotationListItemSchema,
MongoDBCredentialsRotationSchema
} from "./mongodb-credentials-rotation-schemas";
export type TMongoDBCredentialsRotation = z.infer<typeof MongoDBCredentialsRotationSchema>;
export type TMongoDBCredentialsRotationInput = z.infer<typeof CreateMongoDBCredentialsRotationSchema>;
export type TMongoDBCredentialsRotationListItem = z.infer<typeof MongoDBCredentialsRotationListItemSchema>;
export type TMongoDBCredentialsRotationWithConnection = TMongoDBCredentialsRotation & {
connection: TMongoDBConnection;
};
export type TMongoDBCredentialsRotationGeneratedCredentials = z.infer<
typeof MongoDBCredentialsRotationGeneratedCredentialsSchema
>;

View File

@@ -8,7 +8,8 @@ export enum SecretRotation {
AwsIamUserSecret = "aws-iam-user-secret",
LdapPassword = "ldap-password",
OktaClientSecret = "okta-client-secret",
RedisCredentials = "redis-credentials"
RedisCredentials = "redis-credentials",
MongoDBCredentials = "mongodb-credentials"
}
export enum SecretRotationStatus {

View File

@@ -9,6 +9,7 @@ import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret"
import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret";
import { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } from "./ldap-password";
import { MONGODB_CREDENTIALS_ROTATION_LIST_OPTION } from "./mongodb-credentials";
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials";
import { OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./okta-client-secret";
@@ -37,7 +38,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2List
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
[SecretRotation.OktaClientSecret]: OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.RedisCredentials]: REDIS_CREDENTIALS_ROTATION_LIST_OPTION
[SecretRotation.RedisCredentials]: REDIS_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.MongoDBCredentials]: MONGODB_CREDENTIALS_ROTATION_LIST_OPTION
};
export const listSecretRotationOptions = () => {

View File

@@ -11,7 +11,8 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret",
[SecretRotation.LdapPassword]: "LDAP Password",
[SecretRotation.OktaClientSecret]: "Okta Client Secret",
[SecretRotation.RedisCredentials]: "Redis Credentials"
[SecretRotation.RedisCredentials]: "Redis Credentials",
[SecretRotation.MongoDBCredentials]: "MongoDB Credentials"
};
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
@@ -24,5 +25,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
[SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.OktaClientSecret]: AppConnection.Okta,
[SecretRotation.RedisCredentials]: AppConnection.Redis
[SecretRotation.RedisCredentials]: AppConnection.Redis,
[SecretRotation.MongoDBCredentials]: AppConnection.MongoDB
};

View File

@@ -84,6 +84,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns";
import { mongodbCredentialsRotationFactory } from "./mongodb-credentials/mongodb-credentials-rotation-fns";
import { oktaClientSecretRotationFactory } from "./okta-client-secret/okta-client-secret-rotation-fns";
import { redisCredentialsRotationFactory } from "./redis-credentials/redis-credentials-rotation-fns";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
@@ -134,7 +135,8 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
[SecretRotation.OktaClientSecret]: oktaClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.RedisCredentials]: redisCredentialsRotationFactory as TRotationFactoryImplementation
[SecretRotation.RedisCredentials]: redisCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.MongoDBCredentials]: mongodbCredentialsRotationFactory as TRotationFactoryImplementation
};
export const secretRotationV2ServiceFactory = ({

View File

@@ -35,6 +35,12 @@ import {
TLdapPasswordRotationListItem,
TLdapPasswordRotationWithConnection
} from "./ldap-password";
import {
TMongoDBCredentialsRotation,
TMongoDBCredentialsRotationInput,
TMongoDBCredentialsRotationListItem,
TMongoDBCredentialsRotationWithConnection
} from "./mongodb-credentials";
import {
TMsSqlCredentialsRotation,
TMsSqlCredentialsRotationInput,
@@ -86,7 +92,8 @@ export type TSecretRotationV2 =
| TLdapPasswordRotation
| TAwsIamUserSecretRotation
| TOktaClientSecretRotation
| TRedisCredentialsRotation;
| TRedisCredentialsRotation
| TMongoDBCredentialsRotation;
export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection
@@ -98,7 +105,8 @@ export type TSecretRotationV2WithConnection =
| TLdapPasswordRotationWithConnection
| TAwsIamUserSecretRotationWithConnection
| TOktaClientSecretRotationWithConnection
| TRedisCredentialsRotationWithConnection;
| TRedisCredentialsRotationWithConnection
| TMongoDBCredentialsRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials
@@ -119,7 +127,8 @@ export type TSecretRotationV2Input =
| TLdapPasswordRotationInput
| TAwsIamUserSecretRotationInput
| TOktaClientSecretRotationInput
| TRedisCredentialsRotationInput;
| TRedisCredentialsRotationInput
| TMongoDBCredentialsRotationInput;
export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem
@@ -131,7 +140,8 @@ export type TSecretRotationV2ListItem =
| TLdapPasswordRotationListItem
| TAwsIamUserSecretRotationListItem
| TOktaClientSecretRotationListItem
| TRedisCredentialsRotationListItem;
| TRedisCredentialsRotationListItem
| TMongoDBCredentialsRotationListItem;
export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined;

View File

@@ -4,6 +4,7 @@ import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotatio
import { AwsIamUserSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
import { AzureClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret";
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MongoDBCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mongodb-credentials";
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { MySqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials";
import { OktaClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
@@ -21,5 +22,6 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema,
OktaClientSecretRotationSchema,
RedisCredentialsRotationSchema
RedisCredentialsRotationSchema,
MongoDBCredentialsRotationSchema
]);

View File

@@ -85,8 +85,6 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
callback
) => {
// For SQL, since we get existing users, we change both their passwords
// on issue to invalidate their existing passwords
// For SQL, since we get existing users, we change both their passwords
// on issue to invalidate their existing passwords
const credentialsSet = [

View File

@@ -2860,6 +2860,12 @@ export const SecretRotations = {
},
REDIS_CREDENTIALS: {
permissionScope: "The ACL permission scope to assign to the issued Redis users."
},
MONGODB_CREDENTIALS: {
username1:
"The username of the first MongoDB user to rotate passwords for. This user must already exist in your database.",
username2:
"The username of the second MongoDB user to rotate passwords for. This user must already exist in your database."
}
},
SECRETS_MAPPING: {
@@ -2890,6 +2896,10 @@ export const SecretRotations = {
OKTA_CLIENT_SECRET: {
clientId: "The name of the secret that the client ID will be mapped to.",
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
},
MONGODB_CREDENTIALS: {
username: "The name of the secret that the active username will be mapped to.",
password: "The name of the secret that the generated password will be mapped to."
}
}
};

View File

@@ -87,6 +87,10 @@ import {
SanitizedLaravelForgeConnectionSchema
} from "@app/services/app-connection/laravel-forge";
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
import {
MongoDBConnectionListItemSchema,
SanitizedMongoDBConnectionSchema
} from "@app/services/app-connection/mongodb";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql";
import {
@@ -173,6 +177,7 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedOktaConnectionSchema.options,
...SanitizedAzureADCSConnectionSchema.options,
...SanitizedRedisConnectionSchema.options,
...SanitizedMongoDBConnectionSchema.options,
...SanitizedLaravelForgeConnectionSchema.options,
...SanitizedChefConnectionSchema.options,
...SanitizedDNSMadeEasyConnectionSchema.options
@@ -219,6 +224,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
OktaConnectionListItemSchema,
AzureADCSConnectionListItemSchema,
RedisConnectionListItemSchema,
MongoDBConnectionListItemSchema,
LaravelForgeConnectionListItemSchema,
ChefConnectionListItemSchema,
DNSMadeEasyConnectionListItemSchema

View File

@@ -28,6 +28,7 @@ import { registerHerokuConnectionRouter } from "./heroku-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerLaravelForgeConnectionRouter } from "./laravel-forge-connection-router";
import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMongoDBConnectionRouter } from "./mongodb-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
@@ -90,5 +91,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Northflank]: registerNorthflankConnectionRouter,
[AppConnection.Okta]: registerOktaConnectionRouter,
[AppConnection.Redis]: registerRedisConnectionRouter,
[AppConnection.MongoDB]: registerMongoDBConnectionRouter,
[AppConnection.Chef]: registerChefConnectionRouter
};

View File

@@ -0,0 +1,18 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateMongoDBConnectionSchema,
SanitizedMongoDBConnectionSchema,
UpdateMongoDBConnectionSchema
} from "@app/services/app-connection/mongodb";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerMongoDBConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.MongoDB,
server,
sanitizedResponseSchema: SanitizedMongoDBConnectionSchema,
createSchema: CreateMongoDBConnectionSchema,
updateSchema: UpdateMongoDBConnectionSchema
});
};

View File

@@ -39,6 +39,7 @@ export enum AppConnection {
Netlify = "netlify",
Okta = "okta",
Redis = "redis",
MongoDB = "mongodb",
LaravelForge = "laravel-forge",
Chef = "chef",
Northflank = "northflank"

View File

@@ -119,6 +119,7 @@ import {
validateLaravelForgeConnectionCredentials
} from "./laravel-forge";
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
import { getMongoDBConnectionListItem, MongoDBConnectionMethod, validateMongoDBConnectionCredentials } from "./mongodb";
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
@@ -224,6 +225,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
getNorthflankConnectionListItem(),
getOktaConnectionListItem(),
getRedisConnectionListItem(),
getMongoDBConnectionListItem(),
getChefConnectionListItem()
]
.filter((option) => {
@@ -357,7 +359,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.MongoDB]: validateMongoDBConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service);
@@ -411,6 +414,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case OracleDBConnectionMethod.UsernameAndPassword:
case AzureADCSConnectionMethod.UsernamePassword:
case RedisConnectionMethod.UsernameAndPassword:
case MongoDBConnectionMethod.UsernameAndPassword:
return "Username & Password";
case WindmillConnectionMethod.AccessToken:
case HCVaultConnectionMethod.AccessToken:
@@ -504,6 +508,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Northflank]: platformManagedCredentialsNotSupported,
[AppConnection.Okta]: platformManagedCredentialsNotSupported,
[AppConnection.Redis]: platformManagedCredentialsNotSupported,
[AppConnection.MongoDB]: platformManagedCredentialsNotSupported,
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported,
[AppConnection.Chef]: platformManagedCredentialsNotSupported
};

View File

@@ -42,6 +42,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Netlify]: "Netlify",
[AppConnection.Okta]: "Okta",
[AppConnection.Redis]: "Redis",
[AppConnection.MongoDB]: "MongoDB",
[AppConnection.Chef]: "Chef",
[AppConnection.Northflank]: "Northflank"
};
@@ -88,6 +89,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
[AppConnection.Okta]: AppConnectionPlanType.Regular,
[AppConnection.Redis]: AppConnectionPlanType.Regular,
[AppConnection.MongoDB]: AppConnectionPlanType.Regular,
[AppConnection.Chef]: AppConnectionPlanType.Enterprise,
[AppConnection.Northflank]: AppConnectionPlanType.Regular
};

View File

@@ -96,6 +96,7 @@ import { humanitecConnectionService } from "./humanitec/humanitec-connection-ser
import { ValidateLaravelForgeConnectionCredentialsSchema } from "./laravel-forge";
import { laravelForgeConnectionService } from "./laravel-forge/laravel-forge-connection-service";
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMongoDBConnectionCredentialsSchema } from "./mongodb";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
import { ValidateNetlifyConnectionCredentialsSchema } from "./netlify";
@@ -180,6 +181,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Northflank]: ValidateNorthflankConnectionCredentialsSchema,
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema,
[AppConnection.MongoDB]: ValidateMongoDBConnectionCredentialsSchema,
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema
};

View File

@@ -172,6 +172,12 @@ import {
TLdapConnectionInput,
TValidateLdapConnectionCredentialsSchema
} from "./ldap";
import {
TMongoDBConnection,
TMongoDBConnectionConfig,
TMongoDBConnectionInput,
TValidateMongoDBConnectionCredentialsSchema
} from "./mongodb";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { TMySqlConnection, TMySqlConnectionInput, TValidateMySqlConnectionCredentialsSchema } from "./mysql";
import {
@@ -295,6 +301,7 @@ export type TAppConnection = { id: string } & (
| TNorthflankConnection
| TOktaConnection
| TRedisConnection
| TMongoDBConnection
| TChefConnection
);
@@ -345,6 +352,7 @@ export type TAppConnectionInput = { id: string } & (
| TNorthflankConnectionInput
| TOktaConnectionInput
| TRedisConnectionInput
| TMongoDBConnectionInput
| TChefConnectionInput
);
@@ -413,6 +421,7 @@ export type TAppConnectionConfig =
| TNorthflankConnectionConfig
| TOktaConnectionConfig
| TRedisConnectionConfig
| TMongoDBConnectionConfig
| TChefConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
@@ -458,6 +467,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateNorthflankConnectionCredentialsSchema
| TValidateOktaConnectionCredentialsSchema
| TValidateRedisConnectionCredentialsSchema
| TValidateMongoDBConnectionCredentialsSchema
| TValidateChefConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {

View File

@@ -0,0 +1,4 @@
export * from "./mongodb-connection-enums";
export * from "./mongodb-connection-fns";
export * from "./mongodb-connection-schemas";
export * from "./mongodb-connection-types";

View File

@@ -0,0 +1,3 @@
export enum MongoDBConnectionMethod {
UsernameAndPassword = "username-and-password"
}

View File

@@ -0,0 +1,123 @@
import { MongoClient } from "mongodb";
import RE2 from "re2";
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { MongoDBConnectionMethod } from "./mongodb-connection-enums";
import { TMongoDBConnectionConfig } from "./mongodb-connection-types";
export const getMongoDBConnectionListItem = () => {
return {
name: "MongoDB" as const,
app: AppConnection.MongoDB as const,
methods: Object.values(MongoDBConnectionMethod) as [MongoDBConnectionMethod.UsernameAndPassword],
supportsPlatformManagement: false as const
};
};
export type TMongoDBConnectionCredentials = {
host: string;
port?: number;
database: string;
username: string;
password: string;
tlsEnabled?: boolean;
tlsRejectUnauthorized?: boolean;
tlsCertificate?: string;
};
export type TCreateMongoClientOptions = {
authCredentials?: { username: string; password: string };
validateConnection?: boolean;
};
const DEFAULT_CONNECTION_TIMEOUT_MS = 10_000;
export const createMongoClient = async (
credentials: TMongoDBConnectionCredentials,
options?: TCreateMongoClientOptions
): Promise<MongoClient> => {
const srvRegex = new RE2("^mongodb\\+srv:\\/\\/");
const protocolRegex = new RE2("^mongodb:\\/\\/");
let normalizedHost = credentials.host.trim();
const isSrvFromHost = srvRegex.test(normalizedHost);
if (isSrvFromHost) {
normalizedHost = srvRegex.replace(normalizedHost, "");
} else if (protocolRegex.test(normalizedHost)) {
normalizedHost = protocolRegex.replace(normalizedHost, "");
}
const [hostIp] = await verifyHostInputValidity(normalizedHost);
const isSrv = !credentials.port || isSrvFromHost;
const uri = isSrv ? `mongodb+srv://${hostIp}` : `mongodb://${hostIp}:${credentials.port}`;
const authCredentials = options?.authCredentials ?? {
username: credentials.username,
password: credentials.password
};
const clientOptions: {
auth?: { username: string; password?: string };
authSource?: string;
tls?: boolean;
tlsInsecure?: boolean;
ca?: string;
directConnection?: boolean;
connectTimeoutMS?: number;
serverSelectionTimeoutMS?: number;
socketTimeoutMS?: number;
} = {
auth: {
username: authCredentials.username,
password: authCredentials.password
},
authSource: isSrv ? undefined : credentials.database,
directConnection: !isSrv,
connectTimeoutMS: DEFAULT_CONNECTION_TIMEOUT_MS,
serverSelectionTimeoutMS: DEFAULT_CONNECTION_TIMEOUT_MS,
socketTimeoutMS: DEFAULT_CONNECTION_TIMEOUT_MS
};
if (credentials.tlsEnabled) {
clientOptions.tls = true;
clientOptions.tlsInsecure = !credentials.tlsRejectUnauthorized;
if (credentials.tlsCertificate) {
clientOptions.ca = credentials.tlsCertificate;
}
}
const client = new MongoClient(uri, clientOptions);
if (options?.validateConnection) {
await client
.db(credentials.database)
.command({ ping: 1 })
.then(() => true);
}
return client;
};
export const validateMongoDBConnectionCredentials = async (config: TMongoDBConnectionConfig) => {
let client: MongoClient | null = null;
try {
client = await createMongoClient(config.credentials, { validateConnection: true });
if (client) await client.close();
return config.credentials;
} catch (err) {
if (err instanceof BadRequestError) {
throw err;
}
throw new BadRequestError({
message: `Unable to validate connection: ${(err as Error)?.message || "verify credentials"}`
});
} finally {
if (client) await client.close();
}
};

View File

@@ -0,0 +1,89 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { AppConnection } from "../app-connection-enums";
import { MongoDBConnectionMethod } from "./mongodb-connection-enums";
export const BaseMongoDBUsernameAndPasswordConnectionSchema = z.object({
host: z.string().toLowerCase().min(1),
port: z.coerce.number(),
username: z.string().min(1),
password: z.string().min(1),
database: z.string().min(1).trim(),
tlsRejectUnauthorized: z.boolean(),
tlsEnabled: z.boolean(),
tlsCertificate: z
.string()
.trim()
.transform((value) => value || undefined)
.optional()
});
export const MongoDBConnectionAccessTokenCredentialsSchema = BaseMongoDBUsernameAndPasswordConnectionSchema;
const BaseMongoDBConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.MongoDB) });
export const MongoDBConnectionSchema = BaseMongoDBConnectionSchema.extend({
method: z.literal(MongoDBConnectionMethod.UsernameAndPassword),
credentials: MongoDBConnectionAccessTokenCredentialsSchema
});
export const SanitizedMongoDBConnectionSchema = z.discriminatedUnion("method", [
BaseMongoDBConnectionSchema.extend({
method: z.literal(MongoDBConnectionMethod.UsernameAndPassword),
credentials: MongoDBConnectionAccessTokenCredentialsSchema.pick({
host: true,
port: true,
username: true,
database: true,
tlsEnabled: true,
tlsRejectUnauthorized: true,
tlsCertificate: true
})
})
]);
export const ValidateMongoDBConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(MongoDBConnectionMethod.UsernameAndPassword)
.describe(AppConnections.CREATE(AppConnection.MongoDB).method),
credentials: MongoDBConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.MongoDB).credentials
)
})
]);
export const CreateMongoDBConnectionSchema = ValidateMongoDBConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.MongoDB, {
supportsPlatformManagedCredentials: false,
supportsGateways: false
})
);
export const UpdateMongoDBConnectionSchema = z
.object({
credentials: MongoDBConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.MongoDB).credentials
)
})
.and(
GenericUpdateAppConnectionFieldsSchema(AppConnection.MongoDB, {
supportsPlatformManagedCredentials: false,
supportsGateways: false
})
);
export const MongoDBConnectionListItemSchema = z.object({
name: z.literal("MongoDB"),
app: z.literal(AppConnection.MongoDB),
methods: z.nativeEnum(MongoDBConnectionMethod).array(),
supportsPlatformManagement: z.literal(false)
});

View File

@@ -0,0 +1,22 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateMongoDBConnectionSchema,
MongoDBConnectionSchema,
ValidateMongoDBConnectionCredentialsSchema
} from "./mongodb-connection-schemas";
export type TMongoDBConnection = z.infer<typeof MongoDBConnectionSchema>;
export type TMongoDBConnectionInput = z.infer<typeof CreateMongoDBConnectionSchema> & {
app: AppConnection.MongoDB;
};
export type TValidateMongoDBConnectionCredentialsSchema = typeof ValidateMongoDBConnectionCredentialsSchema;
export type TMongoDBConnectionConfig = DiscriminativePick<TMongoDBConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};