From fe3a46a9e7a8561f0ce6dffe1c32b34adbed30cb Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Sat, 20 Sep 2025 06:12:08 +0400
Subject: [PATCH] feat: redis app connection & secret rotation
---
.../v2/secret-rotation-v2-routers/index.ts | 4 +-
.../redis-credentials-rotation-router.ts | 19 ++
.../secret-rotation-v2-router.ts | 4 +-
.../redis-credentials/index.ts | 4 +
.../redis-credentials-rotation-constants.ts | 15 +
.../redis-credentials-rotation-fns.ts | 170 ++++++++++
.../redis-credentials-rotation-schemas.ts | 75 +++++
.../redis-credentials-rotation-types.ts | 24 ++
.../secret-rotation-v2-enums.ts | 3 +-
.../secret-rotation-v2-fns.ts | 4 +-
.../secret-rotation-v2-maps.ts | 6 +-
.../secret-rotation-v2-service.ts | 4 +-
.../secret-rotation-v2-types.ts | 22 +-
.../secret-rotation-v2-union-schema.ts | 4 +-
backend/src/lib/api-docs/constants.ts | 7 +
.../app-connection-router.ts | 7 +-
.../routes/v1/app-connection-routers/index.ts | 4 +-
.../redis-connection-router.ts | 18 +
.../app-connection/app-connection-enums.ts | 3 +-
.../app-connection/app-connection-fns.ts | 11 +-
.../app-connection/app-connection-maps.ts | 6 +-
.../app-connection/app-connection-service.ts | 4 +-
.../app-connection/app-connection-types.ts | 14 +-
.../services/app-connection/redis/index.ts | 4 +
.../redis/redis-connection-enums.ts | 3 +
.../redis/redis-connection-fns.ts | 56 ++++
.../redis/redis-connection-schemas.ts | 87 +++++
.../redis/redis-connection-types.ts | 22 ++
frontend/public/images/integrations/Redis.png | Bin 0 -> 3478 bytes
...redentialsRotationGeneratedCredentials.tsx | 38 +++
...ewSecretRotationV2GeneratedCredentials.tsx | 8 +
...disCredentialsRotationParametersFields.tsx | 197 +++++++++++
.../SecretRotationV2ParametersFields.tsx | 4 +-
.../RedisCredentialsRotationReviewFields.tsx | 50 +++
.../SecretRotationReviewFields.tsx | 4 +-
...redentialsRotationSecretsMappingFields.tsx | 58 ++++
.../SecretRotationV2SecretsMappingFields.tsx | 4 +-
.../forms/schemas/index.ts | 4 +-
.../redis-credentials-rotation-schema.ts | 20 ++
frontend/src/helpers/appConnections.ts | 3 +-
frontend/src/helpers/secretRotationsV2.ts | 11 +-
.../src/hooks/api/appConnections/enums.ts | 3 +-
.../api/appConnections/types/app-options.ts | 5 +
.../hooks/api/appConnections/types/index.ts | 5 +-
.../appConnections/types/redis-connection.ts | 21 ++
.../src/hooks/api/secretRotationsV2/enums.ts | 3 +-
.../api/secretRotationsV2/types/index.ts | 14 +-
.../types/redis-credentials-rotation.ts | 39 +++
.../AppConnectionForm/AppConnectionForm.tsx | 3 +
.../AppConnectionForm/RedisConnectionForm.tsx | 316 ++++++++++++++++++
50 files changed, 1377 insertions(+), 37 deletions(-)
create mode 100644 backend/src/ee/routes/v2/secret-rotation-v2-routers/redis-credentials-rotation-router.ts
create mode 100644 backend/src/ee/services/secret-rotation-v2/redis-credentials/index.ts
create mode 100644 backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-constants.ts
create mode 100644 backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-fns.ts
create mode 100644 backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-schemas.ts
create mode 100644 backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-types.ts
create mode 100644 backend/src/server/routes/v1/app-connection-routers/redis-connection-router.ts
create mode 100644 backend/src/services/app-connection/redis/index.ts
create mode 100644 backend/src/services/app-connection/redis/redis-connection-enums.ts
create mode 100644 backend/src/services/app-connection/redis/redis-connection-fns.ts
create mode 100644 backend/src/services/app-connection/redis/redis-connection-schemas.ts
create mode 100644 backend/src/services/app-connection/redis/redis-connection-types.ts
create mode 100644 frontend/public/images/integrations/Redis.png
create mode 100644 frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewRedisCredentialsRotationGeneratedCredentials.tsx
create mode 100644 frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/RedisCredentialsRotationParametersFields.tsx
create mode 100644 frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/RedisCredentialsRotationReviewFields.tsx
create mode 100644 frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/RedisCredentialsRotationSecretsMappingFields.tsx
create mode 100644 frontend/src/components/secret-rotations-v2/forms/schemas/redis-credentials-rotation-schema.ts
create mode 100644 frontend/src/hooks/api/appConnections/types/redis-connection.ts
create mode 100644 frontend/src/hooks/api/secretRotationsV2/types/redis-credentials-rotation.ts
create mode 100644 frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RedisConnectionForm.tsx
diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
index 5f8dea5d7..8d1702850 100644
--- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
+++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
@@ -9,6 +9,7 @@ import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rota
import { registerOktaClientSecretRotationRouter } from "./okta-client-secret-rotation-router";
import { registerOracleDBCredentialsRotationRouter } from "./oracledb-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
+import { registerRedisCredentialsRotationRouter } from "./redis-credentials-rotation-router";
export * from "./secret-rotation-v2-router";
@@ -24,5 +25,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
[SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
- [SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter
+ [SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter,
+ [SecretRotation.RedisCredentials]: registerRedisCredentialsRotationRouter
};
diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/redis-credentials-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/redis-credentials-rotation-router.ts
new file mode 100644
index 000000000..b83cec52c
--- /dev/null
+++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/redis-credentials-rotation-router.ts
@@ -0,0 +1,19 @@
+import {
+ CreateRedisCredentialsRotationSchema,
+ RedisCredentialsRotationGeneratedCredentialsSchema,
+ RedisCredentialsRotationSchema,
+ UpdateRedisCredentialsRotationSchema
+} from "@app/ee/services/secret-rotation-v2/redis-credentials";
+import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
+
+import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
+
+export const registerRedisCredentialsRotationRouter = async (server: FastifyZodProvider) =>
+ registerSecretRotationEndpoints({
+ type: SecretRotation.RedisCredentials,
+ server,
+ responseSchema: RedisCredentialsRotationSchema,
+ createSchema: CreateRedisCredentialsRotationSchema,
+ updateSchema: UpdateRedisCredentialsRotationSchema,
+ generatedCredentialsSchema: RedisCredentialsRotationGeneratedCredentialsSchema
+ });
diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts
index 7db99c8c4..6ea6497e4 100644
--- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts
+++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts
@@ -10,6 +10,7 @@ import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-
import { OktaClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
import { OracleDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
+import { RedisCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/redis-credentials";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
import { ApiDocsTags, SecretRotations } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter";
@@ -25,7 +26,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
AzureClientSecretRotationListItemSchema,
AwsIamUserSecretRotationListItemSchema,
LdapPasswordRotationListItemSchema,
- OktaClientSecretRotationListItemSchema
+ OktaClientSecretRotationListItemSchema,
+ RedisCredentialsRotationListItemSchema
]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
diff --git a/backend/src/ee/services/secret-rotation-v2/redis-credentials/index.ts b/backend/src/ee/services/secret-rotation-v2/redis-credentials/index.ts
new file mode 100644
index 000000000..2d90beab3
--- /dev/null
+++ b/backend/src/ee/services/secret-rotation-v2/redis-credentials/index.ts
@@ -0,0 +1,4 @@
+export * from "./redis-credentials-rotation-constants";
+export * from "./redis-credentials-rotation-fns";
+export * from "./redis-credentials-rotation-schemas";
+export * from "./redis-credentials-rotation-types";
diff --git a/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-constants.ts
new file mode 100644
index 000000000..1cb14a922
--- /dev/null
+++ b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-constants.ts
@@ -0,0 +1,15 @@
+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 REDIS_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
+ name: "Redis Credentials",
+ type: SecretRotation.RedisCredentials,
+ connection: AppConnection.Redis,
+ template: {
+ secretsMapping: {
+ username: "REDIS_USERNAME",
+ password: "REDIS_PASSWORD"
+ }
+ }
+};
diff --git a/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-fns.ts
new file mode 100644
index 000000000..c321cdeb0
--- /dev/null
+++ b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-fns.ts
@@ -0,0 +1,170 @@
+/* eslint-disable no-await-in-loop */
+import Redis from "ioredis";
+
+import {
+ TRotationFactory,
+ TRotationFactoryGetSecretsPayload,
+ TRotationFactoryIssueCredentials,
+ TRotationFactoryRevokeCredentials,
+ TRotationFactoryRotateCredentials
+} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
+import { BadRequestError } from "@app/lib/errors";
+
+import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../shared/utils";
+import {
+ TRedisCredentialsRotationGeneratedCredentials,
+ TRedisCredentialsRotationWithConnection
+} from "./redis-credentials-rotation-types";
+
+export const redisCredentialsRotationFactory: TRotationFactory<
+ TRedisCredentialsRotationWithConnection,
+ TRedisCredentialsRotationGeneratedCredentials
+> = (secretRotation) => {
+ const { connection, secretsMapping, parameters } = secretRotation;
+
+ const $getClient = async () => {
+ let conn: Redis | null = null;
+ try {
+ conn = new Redis({
+ username: connection.credentials.username,
+ host: connection.credentials.host,
+ port: connection.credentials.port,
+ password: connection.credentials.password,
+ ...(connection.credentials.sslEnabled && {
+ tls: {
+ rejectUnauthorized: connection.credentials.sslRejectUnauthorized,
+ ca: connection.credentials.sslCertificate
+ }
+ })
+ });
+
+ let result: string;
+ if (connection.credentials.password) {
+ result = await conn.auth(connection.credentials.username, connection.credentials.password, () => {});
+ } else {
+ result = await conn.auth(connection.credentials.username, () => {});
+ }
+
+ if (result !== "OK") {
+ throw new BadRequestError({ message: `Invalid credentials, Redis returned ${result} status` });
+ }
+
+ return conn;
+ } catch (err) {
+ if (conn) await conn.quit();
+
+ throw err;
+ }
+ };
+
+ /**
+ * Creates a new user and password for the redis user using ACL
+ */
+ const $rotateAclUser = async () => {
+ const client = await $getClient();
+
+ const username = generatePassword({
+ length: 32,
+ required: {
+ symbols: 0,
+ digits: 5,
+ uppercase: 5,
+ lowercase: 5
+ }
+ });
+
+ const password = generatePassword(parameters.passwordRequirements || DEFAULT_PASSWORD_REQUIREMENTS);
+
+ try {
+ // important: permissionScope is user input so we need to sanitize it, which we do by splitting the permission scope into parts and then passing them to the ACL command as separate arguments
+ const permissionParts = (parameters.permissionScope || "~* +@all").split(" ");
+ await client.call("ACL", "SETUSER", username, `>${password}`, "on", ...permissionParts);
+
+ return {
+ username,
+ password
+ };
+ } catch (error: unknown) {
+ throw new BadRequestError({
+ message: "Unable to validate connection: verify credentials"
+ });
+ }
+ };
+
+ /**
+ * Revokes a ACL password from the Redis server using its username and password.
+ */
+ const revokeCredential = async (username: string) => {
+ const client = await $getClient();
+
+ try {
+ await client.call("ACL", "DELUSER", username);
+ } catch (error: unknown) {
+ throw new BadRequestError({
+ message: "Unable to revoke credential: verify credentials"
+ });
+ }
+ };
+
+ /**
+ * Issues a new set of credentials.
+ */
+ const issueCredentials: TRotationFactoryIssueCredentials = async (
+ callback
+ ) => {
+ const credentials = await $rotateAclUser();
+
+ return callback(credentials);
+ };
+
+ /**
+ * Revokes a list of credentials.
+ */
+ const revokeCredentials: TRotationFactoryRevokeCredentials = async (
+ credentials,
+ callback
+ ) => {
+ if (!credentials?.length) return callback();
+
+ for (const { username } of credentials) {
+ await revokeCredential(username);
+ // eslint-disable-next-line no-promise-executor-return
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ }
+ return callback();
+ };
+
+ /**
+ * Rotates credentials by issuing new ones and revoking the old.
+ */
+ const rotateCredentials: TRotationFactoryRotateCredentials = async (
+ oldCredentials,
+ callback
+ ) => {
+ const newCredentials = await $rotateAclUser();
+
+ if (oldCredentials?.username) {
+ await revokeCredential(oldCredentials.username);
+ }
+
+ return callback(newCredentials);
+ };
+
+ /**
+ * Maps the generated credentials into the secret payload format.
+ */
+ const getSecretsPayload: TRotationFactoryGetSecretsPayload = ({
+ username,
+ password
+ }) => [
+ { key: secretsMapping.username, value: username },
+ { key: secretsMapping.password, value: password }
+ ];
+
+ return {
+ issueCredentials,
+ revokeCredentials,
+ rotateCredentials,
+ getSecretsPayload
+ };
+};
diff --git a/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-schemas.ts
new file mode 100644
index 000000000..4df00f336
--- /dev/null
+++ b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-schemas.ts
@@ -0,0 +1,75 @@
+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 { SecretRotations } from "@app/lib/api-docs";
+import { SecretNameSchema } from "@app/server/lib/schemas";
+import { AppConnection } from "@app/services/app-connection/app-connection-enums";
+
+import { PasswordRequirementsSchema } from "../shared/general";
+
+export const RedisCredentialsRotationGeneratedCredentialsSchema = z
+ .object({
+ username: z.string(),
+ password: z.string()
+ })
+ .array()
+ .min(1)
+ .max(2);
+
+const RedisCredentialsRotationSecretsMappingSchema = z.object({
+ username: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.REDIS_CREDENTIALS.username),
+ password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.REDIS_CREDENTIALS.password)
+});
+
+export const RedisCredentialsRotationParametersSchema = z.object({
+ passwordRequirements: PasswordRequirementsSchema.optional(),
+ permissionScope: z.string().optional().describe(SecretRotations.PARAMETERS.REDIS_CREDENTIALS.permissionScope)
+});
+
+export const RedisCredentialsRotationTemplateSchema = z.object({
+ secretsMapping: z.object({
+ username: z.string(),
+ password: z.string()
+ })
+});
+
+export const RedisCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.RedisCredentials).extend({
+ type: z.literal(SecretRotation.RedisCredentials),
+ parameters: z.object({
+ passwordRequirements: PasswordRequirementsSchema.optional(),
+ permissionScope: z.string().optional()
+ }),
+ secretsMapping: RedisCredentialsRotationSecretsMappingSchema
+});
+
+export const CreateRedisCredentialsRotationSchema = BaseCreateSecretRotationSchema(
+ SecretRotation.RedisCredentials
+).extend({
+ parameters: z.object({
+ passwordRequirements: PasswordRequirementsSchema.optional(),
+ permissionScope: z.string().optional()
+ }),
+ secretsMapping: RedisCredentialsRotationSecretsMappingSchema
+});
+
+export const UpdateRedisCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
+ SecretRotation.RedisCredentials
+).extend({
+ parameters: z.object({
+ passwordRequirements: PasswordRequirementsSchema.optional(),
+ permissionScope: z.string().optional()
+ }),
+ secretsMapping: RedisCredentialsRotationSecretsMappingSchema.optional()
+});
+
+export const RedisCredentialsRotationListItemSchema = z.object({
+ name: z.literal("Redis Credentials"),
+ connection: z.literal(AppConnection.Redis),
+ type: z.literal(SecretRotation.RedisCredentials),
+ template: RedisCredentialsRotationTemplateSchema
+});
diff --git a/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-types.ts
new file mode 100644
index 000000000..46f217d61
--- /dev/null
+++ b/backend/src/ee/services/secret-rotation-v2/redis-credentials/redis-credentials-rotation-types.ts
@@ -0,0 +1,24 @@
+import { z } from "zod";
+
+import { TRedisConnection } from "@app/services/app-connection/redis";
+
+import {
+ CreateRedisCredentialsRotationSchema,
+ RedisCredentialsRotationGeneratedCredentialsSchema,
+ RedisCredentialsRotationListItemSchema,
+ RedisCredentialsRotationSchema
+} from "./redis-credentials-rotation-schemas";
+
+export type TRedisCredentialsRotation = z.infer;
+
+export type TRedisCredentialsRotationInput = z.infer;
+
+export type TRedisCredentialsRotationListItem = z.infer;
+
+export type TRedisCredentialsRotationWithConnection = TRedisCredentialsRotation & {
+ connection: TRedisConnection;
+};
+
+export type TRedisCredentialsRotationGeneratedCredentials = z.infer<
+ typeof RedisCredentialsRotationGeneratedCredentialsSchema
+>;
diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts
index cf0fe578a..661a2399a 100644
--- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts
+++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts
@@ -7,7 +7,8 @@ export enum SecretRotation {
AzureClientSecret = "azure-client-secret",
AwsIamUserSecret = "aws-iam-user-secret",
LdapPassword = "ldap-password",
- OktaClientSecret = "okta-client-secret"
+ OktaClientSecret = "okta-client-secret",
+ RedisCredentials = "redis-credentials"
}
export enum SecretRotationStatus {
diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts
index 4d8cea6a3..e4e6a8531 100644
--- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts
+++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts
@@ -14,6 +14,7 @@ import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials";
import { OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./okta-client-secret";
import { ORACLEDB_CREDENTIALS_ROTATION_LIST_OPTION } from "./oracledb-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
+import { REDIS_CREDENTIALS_ROTATION_LIST_OPTION } from "./redis-credentials";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
import { TSecretRotationV2ServiceFactory, TSecretRotationV2ServiceFactoryDep } from "./secret-rotation-v2-service";
@@ -35,7 +36,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record {
diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts
index d9a771101..2087fa195 100644
--- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts
+++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts
@@ -10,7 +10,8 @@ export const SECRET_ROTATION_NAME_MAP: Record = {
[SecretRotation.AzureClientSecret]: "Azure Client Secret",
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret",
[SecretRotation.LdapPassword]: "LDAP Password",
- [SecretRotation.OktaClientSecret]: "Okta Client Secret"
+ [SecretRotation.OktaClientSecret]: "Okta Client Secret",
+ [SecretRotation.RedisCredentials]: "Redis Credentials"
};
export const SECRET_ROTATION_CONNECTION_MAP: Record = {
@@ -22,5 +23,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record {
diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts
index 70804d173..11d9ce5e6 100644
--- a/backend/src/server/routes/v1/app-connection-routers/index.ts
+++ b/backend/src/server/routes/v1/app-connection-routers/index.ts
@@ -31,6 +31,7 @@ import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
import { registerOktaConnectionRouter } from "./okta-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router";
+import { registerRedisConnectionRouter } from "./redis-connection-router";
import { registerRenderConnectionRouter } from "./render-connection-router";
import { registerSupabaseConnectionRouter } from "./supabase-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
@@ -80,5 +81,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record {
+ registerAppConnectionEndpoints({
+ app: AppConnection.Redis,
+ server,
+ sanitizedResponseSchema: SanitizedRedisConnectionSchema,
+ createSchema: CreateRedisConnectionSchema,
+ updateSchema: UpdateRedisConnectionSchema
+ });
+};
diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts
index 76dcdd5f0..996cd872a 100644
--- a/backend/src/services/app-connection/app-connection-enums.ts
+++ b/backend/src/services/app-connection/app-connection-enums.ts
@@ -36,7 +36,8 @@ export enum AppConnection {
Supabase = "supabase",
DigitalOcean = "digital-ocean",
Netlify = "netlify",
- Okta = "okta"
+ Okta = "okta",
+ Redis = "redis"
}
export enum AWSRegion {
diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts
index f88b5a357..7455a6ac0 100644
--- a/backend/src/services/app-connection/app-connection-fns.ts
+++ b/backend/src/services/app-connection/app-connection-fns.ts
@@ -111,6 +111,7 @@ import { getNetlifyConnectionListItem, validateNetlifyConnectionCredentials } fr
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
+import { getRedisConnectionListItem, RedisConnectionMethod, validateRedisConnectionCredentials } from "./redis";
import { RenderConnectionMethod } from "./render/render-connection-enums";
import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns";
import {
@@ -191,7 +192,8 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
getSupabaseConnectionListItem(),
getDigitalOceanConnectionListItem(),
getNetlifyConnectionListItem(),
- getOktaConnectionListItem()
+ getOktaConnectionListItem(),
+ getRedisConnectionListItem()
]
.filter((option) => {
switch (projectType) {
@@ -317,7 +319,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
- [AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator
+ [AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator,
+ [AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service);
@@ -364,6 +367,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case MySqlConnectionMethod.UsernameAndPassword:
case OracleDBConnectionMethod.UsernameAndPassword:
case AzureADCSConnectionMethod.UsernamePassword:
+ case RedisConnectionMethod.UsernameAndPassword:
return "Username & Password";
case WindmillConnectionMethod.AccessToken:
case HCVaultConnectionMethod.AccessToken:
@@ -451,7 +455,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Supabase]: platformManagedCredentialsNotSupported,
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
[AppConnection.Netlify]: platformManagedCredentialsNotSupported,
- [AppConnection.Okta]: platformManagedCredentialsNotSupported
+ [AppConnection.Okta]: platformManagedCredentialsNotSupported,
+ [AppConnection.Redis]: platformManagedCredentialsNotSupported
};
export const enterpriseAppCheck = async (
diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts
index a2ce02669..e3235d2f7 100644
--- a/backend/src/services/app-connection/app-connection-maps.ts
+++ b/backend/src/services/app-connection/app-connection-maps.ts
@@ -38,7 +38,8 @@ export const APP_CONNECTION_NAME_MAP: Record = {
[AppConnection.Supabase]: "Supabase",
[AppConnection.DigitalOcean]: "DigitalOcean App Platform",
[AppConnection.Netlify]: "Netlify",
- [AppConnection.Okta]: "Okta"
+ [AppConnection.Okta]: "Okta",
+ [AppConnection.Redis]: "Redis"
};
export const APP_CONNECTION_PLAN_MAP: Record = {
@@ -79,5 +80,6 @@ export const APP_CONNECTION_PLAN_MAP: Record>>;
@@ -306,6 +313,7 @@ export type TAppConnectionInput = { id: string } & (
| TDigitalOceanConnectionInput
| TNetlifyConnectionInput
| TOktaConnectionInput
+ | TRedisConnectionInput
);
export type TSqlConnectionInput =
@@ -368,7 +376,8 @@ export type TAppConnectionConfig =
| TSupabaseConnectionConfig
| TDigitalOceanConnectionConfig
| TNetlifyConnectionConfig
- | TOktaConnectionConfig;
+ | TOktaConnectionConfig
+ | TRedisConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -408,7 +417,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateSupabaseConnectionCredentialsSchema
| TValidateDigitalOceanCredentialsSchema
| TValidateNetlifyConnectionCredentialsSchema
- | TValidateOktaConnectionCredentialsSchema;
+ | TValidateOktaConnectionCredentialsSchema
+ | TValidateRedisConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
diff --git a/backend/src/services/app-connection/redis/index.ts b/backend/src/services/app-connection/redis/index.ts
new file mode 100644
index 000000000..76b071958
--- /dev/null
+++ b/backend/src/services/app-connection/redis/index.ts
@@ -0,0 +1,4 @@
+export * from "./redis-connection-enums";
+export * from "./redis-connection-fns";
+export * from "./redis-connection-schemas";
+export * from "./redis-connection-types";
diff --git a/backend/src/services/app-connection/redis/redis-connection-enums.ts b/backend/src/services/app-connection/redis/redis-connection-enums.ts
new file mode 100644
index 000000000..01befeee9
--- /dev/null
+++ b/backend/src/services/app-connection/redis/redis-connection-enums.ts
@@ -0,0 +1,3 @@
+export enum RedisConnectionMethod {
+ UsernameAndPassword = "username-and-password"
+}
diff --git a/backend/src/services/app-connection/redis/redis-connection-fns.ts b/backend/src/services/app-connection/redis/redis-connection-fns.ts
new file mode 100644
index 000000000..901016b3c
--- /dev/null
+++ b/backend/src/services/app-connection/redis/redis-connection-fns.ts
@@ -0,0 +1,56 @@
+import Redis from "ioredis";
+
+import { BadRequestError } from "@app/lib/errors";
+import { AppConnection } from "@app/services/app-connection/app-connection-enums";
+
+import { RedisConnectionMethod } from "./redis-connection-enums";
+import { TRedisConnectionConfig } from "./redis-connection-types";
+
+export const getRedisConnectionListItem = () => {
+ return {
+ name: "Redis" as const,
+ app: AppConnection.Redis as const,
+ methods: Object.values(RedisConnectionMethod) as [RedisConnectionMethod.UsernameAndPassword],
+ supportsPlatformManagement: false as const
+ };
+};
+
+export const validateRedisConnectionCredentials = async (config: TRedisConnectionConfig) => {
+ let connection: Redis | null = null;
+ try {
+ connection = new Redis({
+ username: config.credentials.username,
+ host: config.credentials.host,
+ port: config.credentials.port,
+ password: config.credentials.password,
+ ...(config.credentials.sslEnabled && {
+ tls: {
+ rejectUnauthorized: config.credentials.sslRejectUnauthorized,
+ ca: config.credentials.sslCertificate
+ }
+ })
+ });
+
+ let result: string;
+ if (config.credentials.password) {
+ result = await connection.auth(config.credentials.username, config.credentials.password, () => {});
+ } else {
+ result = await connection.auth(config.credentials.username, () => {});
+ }
+
+ if (result !== "OK") {
+ throw new BadRequestError({ message: `Invalid credentials, Redis returned ${result} status` });
+ }
+
+ 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 (connection) await connection.quit();
+ }
+};
diff --git a/backend/src/services/app-connection/redis/redis-connection-schemas.ts b/backend/src/services/app-connection/redis/redis-connection-schemas.ts
new file mode 100644
index 000000000..60c8b8458
--- /dev/null
+++ b/backend/src/services/app-connection/redis/redis-connection-schemas.ts
@@ -0,0 +1,87 @@
+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 { RedisConnectionMethod } from "./redis-connection-enums";
+
+export const BaseRedisUsernameAndPasswordConnectionSchema = z.object({
+ host: z.string().toLowerCase().min(1),
+ port: z.coerce.number(),
+ username: z.string().min(1),
+ password: z.string().min(1).optional(),
+
+ sslRejectUnauthorized: z.boolean(),
+ sslEnabled: z.boolean(),
+ sslCertificate: z
+ .string()
+ .trim()
+ .transform((value) => value || undefined)
+ .optional()
+});
+
+export const RedisConnectionAccessTokenCredentialsSchema = BaseRedisUsernameAndPasswordConnectionSchema;
+
+const BaseRedisConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Redis) });
+
+export const RedisConnectionSchema = BaseRedisConnectionSchema.extend({
+ method: z.literal(RedisConnectionMethod.UsernameAndPassword),
+ credentials: RedisConnectionAccessTokenCredentialsSchema
+});
+
+export const SanitizedRedisConnectionSchema = z.discriminatedUnion("method", [
+ BaseRedisConnectionSchema.extend({
+ method: z.literal(RedisConnectionMethod.UsernameAndPassword),
+ credentials: RedisConnectionAccessTokenCredentialsSchema.pick({
+ host: true,
+ port: true,
+ username: true,
+ sslEnabled: true,
+ sslRejectUnauthorized: true,
+ sslCertificate: true
+ })
+ })
+]);
+
+export const ValidateRedisConnectionCredentialsSchema = z.discriminatedUnion("method", [
+ z.object({
+ method: z
+ .literal(RedisConnectionMethod.UsernameAndPassword)
+ .describe(AppConnections.CREATE(AppConnection.Redis).method),
+ credentials: RedisConnectionAccessTokenCredentialsSchema.describe(
+ AppConnections.CREATE(AppConnection.Redis).credentials
+ )
+ })
+]);
+
+export const CreateRedisConnectionSchema = ValidateRedisConnectionCredentialsSchema.and(
+ GenericCreateAppConnectionFieldsSchema(AppConnection.Redis, {
+ supportsPlatformManagedCredentials: true,
+ supportsGateways: true
+ })
+);
+
+export const UpdateRedisConnectionSchema = z
+ .object({
+ credentials: RedisConnectionAccessTokenCredentialsSchema.optional().describe(
+ AppConnections.UPDATE(AppConnection.Redis).credentials
+ )
+ })
+ .and(
+ GenericUpdateAppConnectionFieldsSchema(AppConnection.Redis, {
+ supportsPlatformManagedCredentials: true,
+ supportsGateways: true
+ })
+ );
+
+export const RedisConnectionListItemSchema = z.object({
+ name: z.literal("Redis"),
+ app: z.literal(AppConnection.Redis),
+ methods: z.nativeEnum(RedisConnectionMethod).array(),
+ supportsPlatformManagement: z.literal(false)
+});
diff --git a/backend/src/services/app-connection/redis/redis-connection-types.ts b/backend/src/services/app-connection/redis/redis-connection-types.ts
new file mode 100644
index 000000000..2d1ba7699
--- /dev/null
+++ b/backend/src/services/app-connection/redis/redis-connection-types.ts
@@ -0,0 +1,22 @@
+import z from "zod";
+
+import { DiscriminativePick } from "@app/lib/types";
+
+import { AppConnection } from "../app-connection-enums";
+import {
+ CreateRedisConnectionSchema,
+ RedisConnectionSchema,
+ ValidateRedisConnectionCredentialsSchema
+} from "./redis-connection-schemas";
+
+export type TRedisConnection = z.infer;
+
+export type TRedisConnectionInput = z.infer & {
+ app: AppConnection.Redis;
+};
+
+export type TValidateRedisConnectionCredentialsSchema = typeof ValidateRedisConnectionCredentialsSchema;
+
+export type TRedisConnectionConfig = DiscriminativePick & {
+ orgId: string;
+};
diff --git a/frontend/public/images/integrations/Redis.png b/frontend/public/images/integrations/Redis.png
new file mode 100644
index 0000000000000000000000000000000000000000..3ef8adffda10502a274319cf6dec210ea7e005ec
GIT binary patch
literal 3478
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
literal 0
HcmV?d00001
diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewRedisCredentialsRotationGeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewRedisCredentialsRotationGeneratedCredentials.tsx
new file mode 100644
index 000000000..18feefa09
--- /dev/null
+++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewRedisCredentialsRotationGeneratedCredentials.tsx
@@ -0,0 +1,38 @@
+import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
+
+import { ViewRotationGeneratedCredentialsDisplay } from "./shared";
+import { TRedisCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/redis-credentials-rotation";
+
+type Props = {
+ generatedCredentialsResponse: TRedisCredentialsRotationGeneratedCredentialsResponse;
+};
+
+export const ViewRedisCredentialsRotationGeneratedCredentials = ({
+ generatedCredentialsResponse: { generatedCredentials, activeIndex }
+}: Props) => {
+ const inactiveIndex = activeIndex === 0 ? 1 : 0;
+
+ const activeCredentials = generatedCredentials[activeIndex];
+ const inactiveCredentials = generatedCredentials[inactiveIndex];
+
+ return (
+
+ {activeCredentials?.username}
+
+ {activeCredentials?.password}
+
+ >
+ }
+ inactiveCredentials={
+ <>
+ {inactiveCredentials?.username}
+
+ {inactiveCredentials?.password}
+
+ >
+ }
+ />
+ );
+};
diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx
index 33d3fccc1..e8553f6d9 100644
--- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx
+++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx
@@ -23,6 +23,7 @@ import {
import { ViewSqlCredentialsRotationGeneratedCredentials } from "./shared";
import { ViewAwsIamUserSecretRotationGeneratedCredentials } from "./ViewAwsIamUserSecretRotationGeneratedCredentials";
import { ViewOktaClientSecretRotationGeneratedCredentials } from "./ViewOktaClientSecretRotationGeneratedCredentials";
+import { ViewRedisCredentialsRotationGeneratedCredentials } from "./ViewRedisCredentialsRotationGeneratedCredentials";
type Props = {
secretRotation?: TSecretRotationV2;
@@ -107,6 +108,13 @@ const Content = ({ secretRotation }: ContentProps) => {
/>
);
break;
+ case SecretRotation.RedisCredentials:
+ Component = (
+
+ );
+ break;
default:
throw new Error("Unhandled View Generated Credential Rotation Type");
}
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/RedisCredentialsRotationParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/RedisCredentialsRotationParametersFields.tsx
new file mode 100644
index 000000000..2c15b3ca8
--- /dev/null
+++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/RedisCredentialsRotationParametersFields.tsx
@@ -0,0 +1,197 @@
+import { Controller, useFormContext } from "react-hook-form";
+
+import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
+import { FormControl, Input } from "@app/components/v2";
+import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
+import { DEFAULT_PASSWORD_REQUIREMENTS } from "../schemas/shared";
+
+export const RedisCredentialsRotationParametersFields = () => {
+ const { control } = useFormContext<
+ TSecretRotationV2Form & {
+ type: SecretRotation.RedisCredentials;
+ }
+ >();
+
+ return (
+ <>
+
+
(
+
+
+ This is the access control permissions that will be set for the issued Redis
+ users. The format must be a valid Redis ACL pattern.
+
+
+ The default value is{" "}
+
+ ~* +@all
+
+ . You can modify it to suit your needs.
+
+
+ For more information, please refer to the{" "}
+
+ Redis ACL documentation
+
+ .
+
+
+ }
+ label="Permission Scope"
+ isError={Boolean(error)}
+ errorText={error?.message}
+ >
+
+
+ )}
+ />
+
+
+
+ >
+ );
+};
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx
index 3f489b04e..f8f2685ec 100644
--- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx
+++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx
@@ -9,6 +9,7 @@ import { AzureClientSecretRotationParametersFields } from "./AzureClientSecretRo
import { LdapPasswordRotationParametersFields } from "./LdapPasswordRotationParametersFields";
import { OktaClientSecretRotationParametersFields } from "./OktaClientSecretRotationParametersFields";
import { SqlCredentialsRotationParametersFields } from "./shared";
+import { RedisCredentialsRotationParametersFields } from "./RedisCredentialsRotationParametersFields";
const COMPONENT_MAP: Record = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
@@ -19,7 +20,8 @@ const COMPONENT_MAP: Record = {
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields,
- [SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields
+ [SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields,
+ [SecretRotation.RedisCredentials]: RedisCredentialsRotationParametersFields
};
export const SecretRotationV2ParametersFields = () => {
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/RedisCredentialsRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/RedisCredentialsRotationReviewFields.tsx
new file mode 100644
index 000000000..871faf8b6
--- /dev/null
+++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/RedisCredentialsRotationReviewFields.tsx
@@ -0,0 +1,50 @@
+import { useFormContext } from "react-hook-form";
+
+import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
+import { GenericFieldLabel } from "@app/components/v2";
+import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
+
+import { SecretRotationReviewSection } from "./shared";
+
+export const RedisCredentialsRotationReviewFields = () => {
+ const { watch } = useFormContext<
+ TSecretRotationV2Form & {
+ type: SecretRotation.RedisCredentials;
+ }
+ >();
+
+ const [parameters, { username, password }] = watch(["parameters", "secretsMapping"]);
+
+ const { passwordRequirements, permissionScope } = parameters;
+ return (
+ <>
+
+ {permissionScope}
+
+ {passwordRequirements && (
+
+ {passwordRequirements.length}
+
+ {passwordRequirements.required.digits}
+
+
+ {passwordRequirements.required.lowercase}
+
+
+ {passwordRequirements.required.uppercase}
+
+
+ {passwordRequirements.required.symbols}
+
+
+ {passwordRequirements.allowedSymbols}
+
+
+ )}
+
+ {username}
+ {password}
+
+ >
+ );
+};
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx
index 636cc98cc..05b6ad63c 100644
--- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx
+++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx
@@ -12,6 +12,7 @@ import { AzureClientSecretRotationReviewFields } from "./AzureClientSecretRotati
import { LdapPasswordRotationReviewFields } from "./LdapPasswordRotationReviewFields";
import { OktaClientSecretRotationReviewFields } from "./OktaClientSecretRotationReviewFields";
import { SqlCredentialsRotationReviewFields } from "./shared";
+import { RedisCredentialsRotationReviewFields } from "./RedisCredentialsRotationReviewFields";
const COMPONENT_MAP: Record = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields,
@@ -22,7 +23,8 @@ const COMPONENT_MAP: Record = {
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields,
- [SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields
+ [SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields,
+ [SecretRotation.RedisCredentials]: RedisCredentialsRotationReviewFields
};
export const SecretRotationV2ReviewFields = () => {
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/RedisCredentialsRotationSecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/RedisCredentialsRotationSecretsMappingFields.tsx
new file mode 100644
index 000000000..2ffac6a62
--- /dev/null
+++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/RedisCredentialsRotationSecretsMappingFields.tsx
@@ -0,0 +1,58 @@
+import { Controller, useFormContext } from "react-hook-form";
+
+import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
+import { FormControl, Input } from "@app/components/v2";
+import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2";
+
+import { SecretsMappingTable } from "./shared";
+
+export const RedisCredentialsRotationSecretsMappingFields = () => {
+ const { control } = useFormContext<
+ TSecretRotationV2Form & {
+ type: SecretRotation.RedisCredentials;
+ }
+ >();
+
+ const { rotationOption } = useSecretRotationV2Option(SecretRotation.RedisCredentials);
+
+ const items = [
+ {
+ name: "Username",
+ input: (
+ (
+
+
+
+ )}
+ control={control}
+ name="secretsMapping.username"
+ />
+ )
+ },
+ {
+ name: "Password",
+ input: (
+ (
+
+
+
+ )}
+ control={control}
+ name="secretsMapping.password"
+ />
+ )
+ }
+ ];
+
+ return ;
+};
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx
index dd0ce9cab..15338c48a 100644
--- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx
+++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx
@@ -9,6 +9,7 @@ import { AzureClientSecretRotationSecretsMappingFields } from "./AzureClientSecr
import { LdapPasswordRotationSecretsMappingFields } from "./LdapPasswordRotationSecretsMappingFields";
import { OktaClientSecretRotationSecretsMappingFields } from "./OktaClientSecretRotationSecretsMappingFields";
import { SqlCredentialsRotationSecretsMappingFields } from "./shared";
+import { RedisCredentialsRotationSecretsMappingFields } from "./RedisCredentialsRotationSecretsMappingFields";
const COMPONENT_MAP: Record = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields,
@@ -19,7 +20,8 @@ const COMPONENT_MAP: Record = {
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields,
- [SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields
+ [SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields,
+ [SecretRotation.RedisCredentials]: RedisCredentialsRotationSecretsMappingFields
};
export const SecretRotationV2SecretsMappingFields = () => {
diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts
index a6ebe2f64..199036a8f 100644
--- a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts
+++ b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts
@@ -12,6 +12,7 @@ import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/typ
import { OktaClientSecretRotationSchema } from "./okta-client-secret-rotation-schema";
import { OracleDBCredentialsRotationSchema } from "./oracledb-credentials-rotation-schema";
+import { RedisCredentialsRotationSchema } from "./redis-credentials-rotation-schema";
export const SecretRotationV2FormSchema = (isUpdate: boolean) =>
z
@@ -25,7 +26,8 @@ export const SecretRotationV2FormSchema = (isUpdate: boolean) =>
OracleDBCredentialsRotationSchema,
LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema,
- OktaClientSecretRotationSchema
+ OktaClientSecretRotationSchema,
+ RedisCredentialsRotationSchema
]),
z.object({ id: z.string().optional() })
)
diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/redis-credentials-rotation-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/redis-credentials-rotation-schema.ts
new file mode 100644
index 000000000..8e4dad117
--- /dev/null
+++ b/frontend/src/components/secret-rotations-v2/forms/schemas/redis-credentials-rotation-schema.ts
@@ -0,0 +1,20 @@
+import { z } from "zod";
+
+import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema";
+import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
+
+import { PasswordRequirementsSchema } from "./shared";
+
+export const RedisCredentialsRotationSchema = z
+ .object({
+ type: z.literal(SecretRotation.RedisCredentials),
+ parameters: z.object({
+ passwordRequirements: PasswordRequirementsSchema.optional(),
+ permissionScope: z.string().optional()
+ }),
+ secretsMapping: z.object({
+ username: z.string().trim().min(1, "Username required"),
+ password: z.string().trim().min(1, "Password required")
+ })
+ })
+ .merge(BaseSecretRotationSchema);
diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts
index 99103c794..ab7ee7c90 100644
--- a/frontend/src/helpers/appConnections.ts
+++ b/frontend/src/helpers/appConnections.ts
@@ -113,7 +113,8 @@ export const APP_CONNECTION_MAP: Record<
name: "Netlify",
image: "Netlify.png"
},
- [AppConnection.Okta]: { name: "Okta", image: "Okta.png" }
+ [AppConnection.Okta]: { name: "Okta", image: "Okta.png" },
+ [AppConnection.Redis]: { name: "Redis", image: "Redis.png" }
};
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
diff --git a/frontend/src/helpers/secretRotationsV2.ts b/frontend/src/helpers/secretRotationsV2.ts
index 2979a7623..d3bb83f19 100644
--- a/frontend/src/helpers/secretRotationsV2.ts
+++ b/frontend/src/helpers/secretRotationsV2.ts
@@ -49,6 +49,11 @@ export const SECRET_ROTATION_MAP: Record<
name: "Okta Client Secret",
image: "Okta.png",
size: 50
+ },
+ [SecretRotation.RedisCredentials]: {
+ name: "Redis Credentials",
+ image: "Redis.png",
+ size: 50
}
};
@@ -61,7 +66,8 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record = {
[SecretRotation.AzureClientSecret]: true,
[SecretRotation.LdapPassword]: false,
[SecretRotation.AwsIamUserSecret]: true,
- [SecretRotation.OktaClientSecret]: true
+ [SecretRotation.OktaClientSecret]: true,
+ [SecretRotation.RedisCredentials]: true
};
export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => {
diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts
index 7b041b797..e897cf0f0 100644
--- a/frontend/src/hooks/api/appConnections/enums.ts
+++ b/frontend/src/hooks/api/appConnections/enums.ts
@@ -36,5 +36,6 @@ export enum AppConnection {
Supabase = "supabase",
DigitalOcean = "digital-ocean",
Netlify = "netlify",
- Okta = "okta"
+ Okta = "okta",
+ Redis = "redis"
}
diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts
index 67d8feb48..fdaae2c74 100644
--- a/frontend/src/hooks/api/appConnections/types/app-options.ts
+++ b/frontend/src/hooks/api/appConnections/types/app-options.ts
@@ -168,6 +168,10 @@ export type TAzureAdCsConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.AzureADCS;
};
+export type TRedisConnectionOption = TAppConnectionOptionBase & {
+ app: AppConnection.Redis;
+};
+
export type TAppConnectionOption =
| TAwsConnectionOption
| TGitHubConnectionOption
@@ -247,4 +251,5 @@ export type TAppConnectionOptionMap = {
[AppConnection.Netlify]: TNetlifyConnectionOption;
[AppConnection.Okta]: TOktaConnectionOption;
[AppConnection.AzureADCS]: TAzureAdCsConnectionOption;
+ [AppConnection.Redis]: TRedisConnectionOption;
};
diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts
index 8c1d86ca3..9f8df7cfa 100644
--- a/frontend/src/hooks/api/appConnections/types/index.ts
+++ b/frontend/src/hooks/api/appConnections/types/index.ts
@@ -31,6 +31,7 @@ import { TOktaConnection } from "./okta-connection";
import { TOracleDBConnection } from "./oracledb-connection";
import { TPostgresConnection } from "./postgres-connection";
import { TRailwayConnection } from "./railway-connection";
+import { TRedisConnection } from "./redis-connection";
import { TRenderConnection } from "./render-connection";
import { TSupabaseConnection } from "./supabase-connection";
import { TTeamCityConnection } from "./teamcity-connection";
@@ -68,6 +69,7 @@ export * from "./okta-connection";
export * from "./oracledb-connection";
export * from "./postgres-connection";
export * from "./railway-connection";
+export * from "./redis-connection";
export * from "./render-connection";
export * from "./supabase-connection";
export * from "./teamcity-connection";
@@ -114,7 +116,8 @@ export type TAppConnection =
| TSupabaseConnection
| TDigitalOceanConnection
| TNetlifyConnection
- | TOktaConnection;
+ | TOktaConnection
+ | TRedisConnection;
export type TAvailableAppConnection = Pick;
diff --git a/frontend/src/hooks/api/appConnections/types/redis-connection.ts b/frontend/src/hooks/api/appConnections/types/redis-connection.ts
new file mode 100644
index 000000000..efbb78b07
--- /dev/null
+++ b/frontend/src/hooks/api/appConnections/types/redis-connection.ts
@@ -0,0 +1,21 @@
+import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
+
+export enum RedisConnectionMethod {
+ UsernameAndPassword = "username-and-password"
+}
+
+export type TRedisConnectionCredentials = {
+ host: string;
+ port: number;
+ username: string;
+ password?: string;
+ sslEnabled: boolean;
+ sslRejectUnauthorized: boolean;
+ sslCertificate?: string;
+};
+
+export type TRedisConnection = TRootAppConnection & { app: AppConnection.Redis } & {
+ method: RedisConnectionMethod.UsernameAndPassword;
+ credentials: TRedisConnectionCredentials;
+};
diff --git a/frontend/src/hooks/api/secretRotationsV2/enums.ts b/frontend/src/hooks/api/secretRotationsV2/enums.ts
index be692cee3..264a6a4a4 100644
--- a/frontend/src/hooks/api/secretRotationsV2/enums.ts
+++ b/frontend/src/hooks/api/secretRotationsV2/enums.ts
@@ -7,7 +7,8 @@ export enum SecretRotation {
AzureClientSecret = "azure-client-secret",
LdapPassword = "ldap-password",
AwsIamUserSecret = "aws-iam-user-secret",
- OktaClientSecret = "okta-client-secret"
+ OktaClientSecret = "okta-client-secret",
+ RedisCredentials = "redis-credentials"
}
export enum SecretRotationStatus {
diff --git a/frontend/src/hooks/api/secretRotationsV2/types/index.ts b/frontend/src/hooks/api/secretRotationsV2/types/index.ts
index 06783944b..a04b0e020 100644
--- a/frontend/src/hooks/api/secretRotationsV2/types/index.ts
+++ b/frontend/src/hooks/api/secretRotationsV2/types/index.ts
@@ -44,6 +44,11 @@ import {
TOracleDBCredentialsRotation,
TOracleDBCredentialsRotationGeneratedCredentialsResponse
} from "./oracledb-credentials-rotation";
+import {
+ TRedisCredentialsRotation,
+ TRedisCredentialsRotationGeneratedCredentialsResponse,
+ TRedisCredentialsRotationOption
+} from "./redis-credentials-rotation";
export type TSecretRotationV2 = (
| TPostgresCredentialsRotation
@@ -55,6 +60,7 @@ export type TSecretRotationV2 = (
| TLdapPasswordRotation
| TAwsIamUserSecretRotation
| TOktaClientSecretRotation
+ | TRedisCredentialsRotation
) & {
secrets: (SecretV3RawSanitized | null)[];
};
@@ -65,7 +71,8 @@ export type TSecretRotationV2Option =
| TAzureClientSecretRotationOption
| TLdapPasswordRotationOption
| TAwsIamUserSecretRotationOption
- | TOktaClientSecretRotationOption;
+ | TOktaClientSecretRotationOption
+ | TRedisCredentialsRotationOption;
export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] };
@@ -80,7 +87,8 @@ export type TViewSecretRotationGeneratedCredentialsResponse =
| TAzureClientSecretRotationGeneratedCredentialsResponse
| TLdapPasswordRotationGeneratedCredentialsResponse
| TAwsIamUserSecretRotationGeneratedCredentialsResponse
- | TOktaClientSecretRotationGeneratedCredentialsResponse;
+ | TOktaClientSecretRotationGeneratedCredentialsResponse
+ | TRedisCredentialsRotationGeneratedCredentialsResponse;
export type TCreateSecretRotationV2DTO = DiscriminativePick<
TSecretRotationV2,
@@ -133,6 +141,7 @@ export type TSecretRotationOptionMap = {
[SecretRotation.LdapPassword]: TLdapPasswordRotationOption;
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption;
[SecretRotation.OktaClientSecret]: TOktaClientSecretRotationOption;
+ [SecretRotation.RedisCredentials]: TRedisCredentialsRotationOption;
};
export type TSecretRotationGeneratedCredentialsResponseMap = {
@@ -145,4 +154,5 @@ export type TSecretRotationGeneratedCredentialsResponseMap = {
[SecretRotation.LdapPassword]: TLdapPasswordRotationGeneratedCredentialsResponse;
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse;
[SecretRotation.OktaClientSecret]: TOktaClientSecretRotationGeneratedCredentialsResponse;
+ [SecretRotation.RedisCredentials]: TRedisCredentialsRotationGeneratedCredentialsResponse;
};
diff --git a/frontend/src/hooks/api/secretRotationsV2/types/redis-credentials-rotation.ts b/frontend/src/hooks/api/secretRotationsV2/types/redis-credentials-rotation.ts
new file mode 100644
index 000000000..bcbc7a3f4
--- /dev/null
+++ b/frontend/src/hooks/api/secretRotationsV2/types/redis-credentials-rotation.ts
@@ -0,0 +1,39 @@
+import { TPasswordRequirements } from "@app/components/secret-rotations-v2/forms/schemas/shared";
+import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
+import {
+ TSecretRotationV2Base,
+ TSecretRotationV2GeneratedCredentialsResponseBase
+} from "@app/hooks/api/secretRotationsV2/types/shared";
+
+export type TRedisCredentialsRotation = TSecretRotationV2Base & {
+ type: SecretRotation.RedisCredentials;
+ parameters: {
+ passwordRequirements?: TPasswordRequirements;
+ permissionScope?: string;
+ };
+ secretsMapping: {
+ username: string;
+ password: string;
+ };
+};
+
+export type TRedisCredentialsRotationGeneratedCredentials = {
+ username: string;
+ password: string;
+};
+
+export type TRedisCredentialsRotationGeneratedCredentialsResponse =
+ TSecretRotationV2GeneratedCredentialsResponseBase<
+ SecretRotation.RedisCredentials,
+ TRedisCredentialsRotationGeneratedCredentials
+ >;
+
+export type TRedisCredentialsRotationOption = {
+ name: string;
+ type: SecretRotation.RedisCredentials;
+ connection: AppConnection.Redis;
+ template: {
+ secretsMapping: TRedisCredentialsRotation["secretsMapping"];
+ };
+};
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx
index f82e4107d..fdb820962 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx
@@ -47,6 +47,7 @@ import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm";
import { VercelConnectionForm } from "./VercelConnectionForm";
import { WindmillConnectionForm } from "./WindmillConnectionForm";
import { ZabbixConnectionForm } from "./ZabbixConnectionForm";
+import { RedisConnectionForm } from "./RedisConnectionForm";
type FormProps = {
onComplete: (appConnection: TAppConnection) => void;
@@ -167,6 +168,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
return ;
case AppConnection.Okta:
return ;
+ case AppConnection.Redis:
+ return ;
default:
throw new Error(`Unhandled App ${app}`);
}
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RedisConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RedisConnectionForm.tsx
new file mode 100644
index 000000000..602b9486c
--- /dev/null
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RedisConnectionForm.tsx
@@ -0,0 +1,316 @@
+import { useState } from "react";
+import { Controller, FormProvider, useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import { Tab } from "@headlessui/react";
+import {
+ Button,
+ FormControl,
+ Input,
+ ModalClose,
+ SecretInput,
+ Select,
+ SelectItem,
+ Switch,
+ TextArea,
+ Tooltip
+} from "@app/components/v2";
+import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
+import { RedisConnectionMethod, TRedisConnection } from "@app/hooks/api/appConnections";
+import { AppConnection } from "@app/hooks/api/appConnections/enums";
+
+import {
+ genericAppConnectionFieldsSchema,
+ GenericAppConnectionsFields
+} from "./GenericAppConnectionFields";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
+
+type Props = {
+ appConnection?: TRedisConnection;
+ onSubmit: (formData: FormData) => Promise;
+};
+
+const rootSchema = genericAppConnectionFieldsSchema.extend({
+ app: z.literal(AppConnection.Redis)
+});
+
+const formSchema = z.discriminatedUnion("method", [
+ rootSchema.extend({
+ method: z.literal(RedisConnectionMethod.UsernameAndPassword),
+ credentials: z.object({
+ host: z.string().trim().min(1, "Host required"),
+ port: z.coerce.number().default(6379),
+ username: z.string().trim().min(1, "Username required"),
+ password: z.string().trim().optional(),
+ sslEnabled: z.boolean().default(false),
+ sslRejectUnauthorized: z.boolean().default(true),
+ sslCertificate: z
+ .string()
+ .trim()
+ .transform((value) => value || undefined)
+ .optional()
+ })
+ })
+]);
+
+type FormData = z.infer;
+
+export const RedisConnectionForm = ({ appConnection, onSubmit }: Props) => {
+ const isUpdate = Boolean(appConnection);
+ const [selectedTabIndex, setSelectedTabIndex] = useState(0);
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: appConnection ?? {
+ app: AppConnection.Redis,
+ method: RedisConnectionMethod.UsernameAndPassword,
+ credentials: {
+ host: "",
+ port: 6379,
+ username: "",
+ password: "",
+ sslEnabled: false,
+ sslRejectUnauthorized: true,
+ sslCertificate: undefined
+ }
+ }
+ });
+
+ const {
+ handleSubmit,
+ watch,
+ control,
+ formState: { isSubmitting, isDirty }
+ } = form;
+
+ const sslEnabled = watch("credentials.sslEnabled");
+
+ return (
+
+
+
+
+ )}
+ />
+
+
+
+ >
+
+
+
+
+
+
+
+
+
+ );
+};