mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: redis app connection & secret rotation
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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";
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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<TRedisCredentialsRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotateAclUser();
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
/**
|
||||
* Revokes a list of credentials.
|
||||
*/
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials<TRedisCredentialsRotationGeneratedCredentials> = 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<TRedisCredentialsRotationGeneratedCredentials> = 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<TRedisCredentialsRotationGeneratedCredentials> = ({
|
||||
username,
|
||||
password
|
||||
}) => [
|
||||
{ key: secretsMapping.username, value: username },
|
||||
{ key: secretsMapping.password, value: password }
|
||||
];
|
||||
|
||||
return {
|
||||
issueCredentials,
|
||||
revokeCredentials,
|
||||
rotateCredentials,
|
||||
getSecretsPayload
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
});
|
||||
@@ -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<typeof RedisCredentialsRotationSchema>;
|
||||
|
||||
export type TRedisCredentialsRotationInput = z.infer<typeof CreateRedisCredentialsRotationSchema>;
|
||||
|
||||
export type TRedisCredentialsRotationListItem = z.infer<typeof RedisCredentialsRotationListItemSchema>;
|
||||
|
||||
export type TRedisCredentialsRotationWithConnection = TRedisCredentialsRotation & {
|
||||
connection: TRedisConnection;
|
||||
};
|
||||
|
||||
export type TRedisCredentialsRotationGeneratedCredentials = z.infer<
|
||||
typeof RedisCredentialsRotationGeneratedCredentialsSchema
|
||||
>;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<SecretRotation, TSecretRotationV2List
|
||||
[SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
||||
[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.OktaClientSecret]: OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.RedisCredentials]: REDIS_CREDENTIALS_ROTATION_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretRotationOptions = () => {
|
||||
|
||||
@@ -10,7 +10,8 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||
[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<SecretRotation, AppConnection> = {
|
||||
@@ -22,5 +23,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
|
||||
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
|
||||
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
|
||||
[SecretRotation.LdapPassword]: AppConnection.LDAP,
|
||||
[SecretRotation.OktaClientSecret]: AppConnection.Okta
|
||||
[SecretRotation.OktaClientSecret]: AppConnection.Okta,
|
||||
[SecretRotation.RedisCredentials]: AppConnection.Redis
|
||||
};
|
||||
|
||||
@@ -85,6 +85,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 { 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";
|
||||
|
||||
export type TSecretRotationV2ServiceFactoryDep = {
|
||||
@@ -132,7 +133,8 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
|
||||
[SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.OktaClientSecret]: oktaClientSecretRotationFactory as TRotationFactoryImplementation
|
||||
[SecretRotation.OktaClientSecret]: oktaClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.RedisCredentials]: redisCredentialsRotationFactory as TRotationFactoryImplementation
|
||||
};
|
||||
|
||||
export const secretRotationV2ServiceFactory = ({
|
||||
|
||||
@@ -66,6 +66,13 @@ import {
|
||||
TPostgresCredentialsRotationListItem,
|
||||
TPostgresCredentialsRotationWithConnection
|
||||
} from "./postgres-credentials";
|
||||
import {
|
||||
TRedisCredentialsRotation,
|
||||
TRedisCredentialsRotationGeneratedCredentials,
|
||||
TRedisCredentialsRotationInput,
|
||||
TRedisCredentialsRotationListItem,
|
||||
TRedisCredentialsRotationWithConnection
|
||||
} from "./redis-credentials/redis-credentials-rotation-types";
|
||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||
import { SecretRotation } from "./secret-rotation-v2-enums";
|
||||
|
||||
@@ -78,7 +85,8 @@ export type TSecretRotationV2 =
|
||||
| TAzureClientSecretRotation
|
||||
| TLdapPasswordRotation
|
||||
| TAwsIamUserSecretRotation
|
||||
| TOktaClientSecretRotation;
|
||||
| TOktaClientSecretRotation
|
||||
| TRedisCredentialsRotation;
|
||||
|
||||
export type TSecretRotationV2WithConnection =
|
||||
| TPostgresCredentialsRotationWithConnection
|
||||
@@ -89,7 +97,8 @@ export type TSecretRotationV2WithConnection =
|
||||
| TAzureClientSecretRotationWithConnection
|
||||
| TLdapPasswordRotationWithConnection
|
||||
| TAwsIamUserSecretRotationWithConnection
|
||||
| TOktaClientSecretRotationWithConnection;
|
||||
| TOktaClientSecretRotationWithConnection
|
||||
| TRedisCredentialsRotationWithConnection;
|
||||
|
||||
export type TSecretRotationV2GeneratedCredentials =
|
||||
| TSqlCredentialsRotationGeneratedCredentials
|
||||
@@ -97,7 +106,8 @@ export type TSecretRotationV2GeneratedCredentials =
|
||||
| TAzureClientSecretRotationGeneratedCredentials
|
||||
| TLdapPasswordRotationGeneratedCredentials
|
||||
| TAwsIamUserSecretRotationGeneratedCredentials
|
||||
| TOktaClientSecretRotationGeneratedCredentials;
|
||||
| TOktaClientSecretRotationGeneratedCredentials
|
||||
| TRedisCredentialsRotationGeneratedCredentials;
|
||||
|
||||
export type TSecretRotationV2Input =
|
||||
| TPostgresCredentialsRotationInput
|
||||
@@ -108,7 +118,8 @@ export type TSecretRotationV2Input =
|
||||
| TAzureClientSecretRotationInput
|
||||
| TLdapPasswordRotationInput
|
||||
| TAwsIamUserSecretRotationInput
|
||||
| TOktaClientSecretRotationInput;
|
||||
| TOktaClientSecretRotationInput
|
||||
| TRedisCredentialsRotationInput;
|
||||
|
||||
export type TSecretRotationV2ListItem =
|
||||
| TPostgresCredentialsRotationListItem
|
||||
@@ -119,7 +130,8 @@ export type TSecretRotationV2ListItem =
|
||||
| TAzureClientSecretRotationListItem
|
||||
| TLdapPasswordRotationListItem
|
||||
| TAwsIamUserSecretRotationListItem
|
||||
| TOktaClientSecretRotationListItem;
|
||||
| TOktaClientSecretRotationListItem
|
||||
| TRedisCredentialsRotationListItem;
|
||||
|
||||
export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { MySqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation
|
||||
import { OktaClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
|
||||
import { OracleDBCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
|
||||
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
import { RedisCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/redis-credentials";
|
||||
|
||||
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationSchema,
|
||||
@@ -19,5 +20,6 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||
AzureClientSecretRotationSchema,
|
||||
LdapPasswordRotationSchema,
|
||||
AwsIamUserSecretRotationSchema,
|
||||
OktaClientSecretRotationSchema
|
||||
OktaClientSecretRotationSchema,
|
||||
RedisCredentialsRotationSchema
|
||||
]);
|
||||
|
||||
@@ -2685,9 +2685,16 @@ export const SecretRotations = {
|
||||
},
|
||||
OKTA_CLIENT_SECRET: {
|
||||
clientId: "The ID of the Okta Application to rotate the client secret for."
|
||||
},
|
||||
REDIS_CREDENTIALS: {
|
||||
permissionScope: "The ACL permission scope to assign to the issued Redis users."
|
||||
}
|
||||
},
|
||||
SECRETS_MAPPING: {
|
||||
REDIS_CREDENTIALS: {
|
||||
username: "The name of the secret that the username will be mapped to.",
|
||||
password: "The name of the secret that the rotated password will be mapped to."
|
||||
},
|
||||
SQL_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."
|
||||
|
||||
@@ -93,6 +93,7 @@ import {
|
||||
RailwayConnectionListItemSchema,
|
||||
SanitizedRailwayConnectionSchema
|
||||
} from "@app/services/app-connection/railway";
|
||||
import { RedisConnectionListItemSchema, SanitizedRedisConnectionSchema } from "@app/services/app-connection/redis";
|
||||
import {
|
||||
RenderConnectionListItemSchema,
|
||||
SanitizedRenderConnectionSchema
|
||||
@@ -156,7 +157,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedDigitalOceanConnectionSchema.options,
|
||||
...SanitizedNetlifyConnectionSchema.options,
|
||||
...SanitizedOktaConnectionSchema.options,
|
||||
...SanitizedAzureADCSConnectionSchema.options
|
||||
...SanitizedAzureADCSConnectionSchema.options,
|
||||
...SanitizedRedisConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -197,7 +199,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
DigitalOceanConnectionListItemSchema,
|
||||
NetlifyConnectionListItemSchema,
|
||||
OktaConnectionListItemSchema,
|
||||
AzureADCSConnectionListItemSchema
|
||||
AzureADCSConnectionListItemSchema,
|
||||
RedisConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -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<AppConnection, (server:
|
||||
[AppConnection.Supabase]: registerSupabaseConnectionRouter,
|
||||
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
|
||||
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
|
||||
[AppConnection.Okta]: registerOktaConnectionRouter
|
||||
[AppConnection.Okta]: registerOktaConnectionRouter,
|
||||
[AppConnection.Redis]: registerRedisConnectionRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateRedisConnectionSchema,
|
||||
SanitizedRedisConnectionSchema,
|
||||
UpdateRedisConnectionSchema
|
||||
} from "@app/services/app-connection/redis";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerRedisConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Redis,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedRedisConnectionSchema,
|
||||
createSchema: CreateRedisConnectionSchema,
|
||||
updateSchema: UpdateRedisConnectionSchema
|
||||
});
|
||||
};
|
||||
@@ -36,7 +36,8 @@ export enum AppConnection {
|
||||
Supabase = "supabase",
|
||||
DigitalOcean = "digital-ocean",
|
||||
Netlify = "netlify",
|
||||
Okta = "okta"
|
||||
Okta = "okta",
|
||||
Redis = "redis"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -38,7 +38,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[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<AppConnection, AppConnectionPlanType> = {
|
||||
@@ -79,5 +80,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Supabase]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.DigitalOcean]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Okta]: AppConnectionPlanType.Regular
|
||||
[AppConnection.Okta]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Redis]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -99,6 +99,7 @@ import { oktaConnectionService } from "./okta/okta-connection-service";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
import { ValidateRailwayConnectionCredentialsSchema } from "./railway";
|
||||
import { railwayConnectionService } from "./railway/railway-connection-service";
|
||||
import { ValidateRedisConnectionCredentialsSchema } from "./redis";
|
||||
import { ValidateRenderConnectionCredentialsSchema } from "./render/render-connection-schema";
|
||||
import { renderConnectionService } from "./render/render-connection-service";
|
||||
import { ValidateSupabaseConnectionCredentialsSchema } from "./supabase";
|
||||
@@ -166,7 +167,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
|
||||
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
|
||||
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema
|
||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
|
||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
|
||||
@@ -179,6 +179,12 @@ import {
|
||||
TRailwayConnectionInput,
|
||||
TValidateRailwayConnectionCredentialsSchema
|
||||
} from "./railway";
|
||||
import {
|
||||
TRedisConnection,
|
||||
TRedisConnectionConfig,
|
||||
TRedisConnectionInput,
|
||||
TValidateRedisConnectionCredentialsSchema
|
||||
} from "./redis";
|
||||
import {
|
||||
TRenderConnection,
|
||||
TRenderConnectionConfig,
|
||||
@@ -261,6 +267,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TDigitalOceanConnection
|
||||
| TNetlifyConnection
|
||||
| TOktaConnection
|
||||
| TRedisConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -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;
|
||||
|
||||
4
backend/src/services/app-connection/redis/index.ts
Normal file
4
backend/src/services/app-connection/redis/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./redis-connection-enums";
|
||||
export * from "./redis-connection-fns";
|
||||
export * from "./redis-connection-schemas";
|
||||
export * from "./redis-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum RedisConnectionMethod {
|
||||
UsernameAndPassword = "username-and-password"
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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)
|
||||
});
|
||||
@@ -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<typeof RedisConnectionSchema>;
|
||||
|
||||
export type TRedisConnectionInput = z.infer<typeof CreateRedisConnectionSchema> & {
|
||||
app: AppConnection.Redis;
|
||||
};
|
||||
|
||||
export type TValidateRedisConnectionCredentialsSchema = typeof ValidateRedisConnectionCredentialsSchema;
|
||||
|
||||
export type TRedisConnectionConfig = DiscriminativePick<TRedisConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
Reference in New Issue
Block a user