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:
BIN
frontend/public/images/integrations/Redis.png
Normal file
BIN
frontend/public/images/integrations/Redis.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
@@ -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 (
|
||||
<ViewRotationGeneratedCredentialsDisplay
|
||||
activeCredentials={
|
||||
<>
|
||||
<CredentialDisplay label="Username">{activeCredentials?.username}</CredentialDisplay>
|
||||
<CredentialDisplay isSensitive label="Password">
|
||||
{activeCredentials?.password}
|
||||
</CredentialDisplay>
|
||||
</>
|
||||
}
|
||||
inactiveCredentials={
|
||||
<>
|
||||
<CredentialDisplay label="Username">{inactiveCredentials?.username}</CredentialDisplay>
|
||||
<CredentialDisplay isSensitive label="Password">
|
||||
{inactiveCredentials?.password}
|
||||
</CredentialDisplay>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 = (
|
||||
<ViewRedisCredentialsRotationGeneratedCredentials
|
||||
generatedCredentialsResponse={generatedCredentialsResponse}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unhandled View Generated Credential Rotation Type");
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parameters.permissionScope"
|
||||
defaultValue={""}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipClassName="max-w-[40rem] w-full"
|
||||
tooltipText={
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>
|
||||
This is the access control permissions that will be set for the issued Redis
|
||||
users. The format must be a valid Redis ACL pattern.
|
||||
</p>
|
||||
<p>
|
||||
The default value is{" "}
|
||||
<code className="rounded bg-mineshaft-700 px-1 py-0.5 font-mono font-medium text-bunker-300">
|
||||
~* +@all
|
||||
</code>
|
||||
. You can modify it to suit your needs.
|
||||
</p>
|
||||
<p>
|
||||
For more information, please refer to the{" "}
|
||||
<a
|
||||
className="font-medium text-primary-500 underline hover:text-primary-600"
|
||||
href="https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Redis ACL documentation
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
label="Permission Scope"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="~* +@all" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Password Requirements</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-3 gap-y-1 rounded border border-mineshaft-600 bg-mineshaft-700 px-3 pt-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name="parameters.passwordRequirements.length"
|
||||
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.length}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password Length"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The length of the password to generate"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={250}
|
||||
size="sm"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parameters.passwordRequirements.required.digits"
|
||||
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.digits}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Digit Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="Minimum number of digits"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
size="sm"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parameters.passwordRequirements.required.lowercase"
|
||||
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.lowercase}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Lowercase Character Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="Minimum number of lowercase characters"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
size="sm"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parameters.passwordRequirements.required.uppercase"
|
||||
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.uppercase}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Uppercase Character Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="Minimum number of uppercase characters"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
size="sm"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parameters.passwordRequirements.required.symbols"
|
||||
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.symbols}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Symbol Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="Minimum number of symbols"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
size="sm"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parameters.passwordRequirements.allowedSymbols"
|
||||
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.allowedSymbols}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Symbols"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="Symbols to use in generated password"
|
||||
>
|
||||
<Input
|
||||
placeholder="-_.~!*"
|
||||
size="sm"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
|
||||
@@ -19,7 +20,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields,
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields,
|
||||
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields
|
||||
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields,
|
||||
[SecretRotation.RedisCredentials]: RedisCredentialsRotationParametersFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2ParametersFields = () => {
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<SecretRotationReviewSection label="Parameters">
|
||||
<GenericFieldLabel label="Permission Scope">{permissionScope}</GenericFieldLabel>
|
||||
</SecretRotationReviewSection>
|
||||
{passwordRequirements && (
|
||||
<SecretRotationReviewSection label="Password Requirements">
|
||||
<GenericFieldLabel label="Length">{passwordRequirements.length}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Minimum Digits">
|
||||
{passwordRequirements.required.digits}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Minimum Lowercase Characters">
|
||||
{passwordRequirements.required.lowercase}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Minimum Uppercase Characters">
|
||||
{passwordRequirements.required.uppercase}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Minimum Symbols">
|
||||
{passwordRequirements.required.symbols}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Allowed Symbols">
|
||||
{passwordRequirements.allowedSymbols}
|
||||
</GenericFieldLabel>
|
||||
</SecretRotationReviewSection>
|
||||
)}
|
||||
<SecretRotationReviewSection label="Secrets Mapping">
|
||||
<GenericFieldLabel label="Username">{username}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Password">{password}</GenericFieldLabel>
|
||||
</SecretRotationReviewSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields,
|
||||
@@ -22,7 +23,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields,
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields,
|
||||
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields
|
||||
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields,
|
||||
[SecretRotation.RedisCredentials]: RedisCredentialsRotationReviewFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2ReviewFields = () => {
|
||||
|
||||
@@ -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: (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={rotationOption?.template.secretsMapping.username}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="secretsMapping.username"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "Password",
|
||||
input: (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={rotationOption?.template.secretsMapping.password}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="secretsMapping.password"
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return <SecretsMappingTable items={items} />;
|
||||
};
|
||||
@@ -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, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields,
|
||||
@@ -19,7 +20,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields,
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields
|
||||
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.RedisCredentials]: RedisCredentialsRotationSecretsMappingFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2SecretsMappingFields = () => {
|
||||
|
||||
@@ -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() })
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
@@ -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"]) => {
|
||||
|
||||
@@ -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, AppConnectio
|
||||
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
|
||||
[SecretRotation.LdapPassword]: AppConnection.LDAP,
|
||||
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
|
||||
[SecretRotation.OktaClientSecret]: AppConnection.Okta
|
||||
[SecretRotation.OktaClientSecret]: AppConnection.Okta,
|
||||
[SecretRotation.RedisCredentials]: AppConnection.Redis
|
||||
};
|
||||
|
||||
// if a rotation can potentially have downtime due to rotating a single credential set this to false
|
||||
@@ -74,7 +80,8 @@ export const IS_ROTATION_DUAL_CREDENTIALS: Record<SecretRotation, boolean> = {
|
||||
[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"]) => {
|
||||
|
||||
@@ -36,5 +36,6 @@ export enum AppConnection {
|
||||
Supabase = "supabase",
|
||||
DigitalOcean = "digital-ocean",
|
||||
Netlify = "netlify",
|
||||
Okta = "okta"
|
||||
Okta = "okta",
|
||||
Redis = "redis"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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<TAppConnection, "name" | "id" | "projectId">;
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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"];
|
||||
};
|
||||
};
|
||||
@@ -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 <NetlifyConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Okta:
|
||||
return <OktaConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Redis:
|
||||
return <RedisConnectionForm onSubmit={onSubmit} />;
|
||||
default:
|
||||
throw new Error(`Unhandled App ${app}`);
|
||||
}
|
||||
|
||||
@@ -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<void>;
|
||||
};
|
||||
|
||||
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<typeof formSchema>;
|
||||
|
||||
export const RedisConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
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 (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The method you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.Redis].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Method"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(RedisConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}{" "}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<>
|
||||
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
||||
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected
|
||||
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
||||
: "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Configuration
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected
|
||||
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
||||
: "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
>
|
||||
SSL ({sslEnabled ? "Enabled" : "Disabled"})
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels className="mb-4 rounded border border-mineshaft-600 bg-mineshaft-700/70 p-3 pb-0">
|
||||
<Tab.Panel>
|
||||
<div className="mt-[0.675rem] flex items-start gap-2">
|
||||
<Controller
|
||||
name="credentials.host"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="flex-1"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Host"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.port"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="w-28"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Port"
|
||||
>
|
||||
<Input type="number" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.675rem] flex items-start gap-2">
|
||||
<Controller
|
||||
name="credentials.username"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Username"
|
||||
className="flex-1"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.password"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Password"
|
||||
className="flex-1"
|
||||
>
|
||||
<SecretInput
|
||||
containerClassName="text-gray-400 w-full group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<Controller
|
||||
name="credentials.sslEnabled"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="ssl-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
>
|
||||
Enable SSL
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.sslCertificate"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
className={sslEnabled ? "" : "opacity-50"}
|
||||
label="SSL Certificate"
|
||||
isOptional
|
||||
>
|
||||
<TextArea
|
||||
className="h-[3.5rem] !resize-none"
|
||||
{...field}
|
||||
isDisabled={!sslEnabled}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.sslRejectUnauthorized"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className={sslEnabled ? "" : "opacity-50"}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="ssl-reject-unauthorized"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={sslEnabled ? value : false}
|
||||
onCheckedChange={onChange}
|
||||
isDisabled={!sslEnabled}
|
||||
>
|
||||
<p className="w-[9.5rem]">
|
||||
Reject Unauthorized
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<p>
|
||||
If enabled, Infisical will only connect to the server if it has a
|
||||
valid, trusted SSL certificate.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</>
|
||||
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Credentials" : "Connect to Database"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user