Add AWS secret rotation V2

This commit is contained in:
carlosmonastyrski
2025-04-22 13:26:48 -03:00
parent 35f0e8f49a
commit 2e3330bf69
50 changed files with 897 additions and 140 deletions
@@ -0,0 +1,83 @@
import { Controller, useFormContext } from "react-hook-form";
import { SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
import { AwsRegionSelect } from "@app/components/secret-syncs/forms/SecretSyncDestinationFields/shared";
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
import { TAwsIamUserSecret, useListAwsConnectionIamUsers } from "@app/hooks/api/appConnections/aws";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
export const AwsIamUserSecretRotationParametersFields = () => {
const { control, watch } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.AwsIamUserSecret;
}
>();
const connectionId = watch("connection.id");
const { data: clients, isPending: isClientsPending } = useListAwsConnectionIamUsers({
connectionId
});
return (
<>
<Controller
name="parameters.clientName"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="IAM User"
helperText={
<Tooltip
className="max-w-md"
content={
<>
Ensure that your connection has the correct permissions and the IAM user exists
in the connection&#39;s audience.
</>
}
>
<div>
<span>Don&#39;t see the IAM user you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isClientsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={clients?.find((client) => client.UserName === value) ?? ""}
onChange={(option) => {
onChange((option as SingleValue<TAwsIamUserSecret>)?.UserName ?? "");
}}
options={clients}
placeholder="Select an IAM user..."
getOptionLabel={(option) =>
(option as SingleValue<TAwsIamUserSecret>)?.UserName ?? ""
}
getOptionValue={(option) =>
(option as SingleValue<TAwsIamUserSecret>)?.UserName ?? ""
}
/>
</FormControl>
)}
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Region">
<AwsRegionSelect value={value} onChange={onChange} />
</FormControl>
)}
control={control}
name="parameters.region"
/>
</>
);
};
@@ -4,12 +4,14 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { TSecretRotationV2Form } from "../schemas";
import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRotationParametersFields";
import { AwsIamUserSecretRotationParametersFields } from "./AwsIamUserSecretRotationParametersFields";
import { SqlCredentialsRotationParametersFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields
};
export const SecretRotationV2ParametersFields = () => {
@@ -0,0 +1,30 @@
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 AwsIamUserSecretRotationReviewFields = () => {
const { watch } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.AwsIamUserSecret;
}
>();
const [parameters, { accessKeyId, secretAccessKey }] = watch(["parameters", "secretsMapping"]);
return (
<>
<SecretRotationReviewSection label="Parameters">
<GenericFieldLabel label="Region">{parameters.region}</GenericFieldLabel>
<GenericFieldLabel label="User Name">{parameters.clientName}</GenericFieldLabel>
</SecretRotationReviewSection>
<SecretRotationReviewSection label="Secrets Mapping">
<GenericFieldLabel label="Access Key ID">{accessKeyId}</GenericFieldLabel>
<GenericFieldLabel label="Access Key Secret">{secretAccessKey}</GenericFieldLabel>
</SecretRotationReviewSection>
</>
);
};
@@ -7,12 +7,14 @@ import { getRotateAtLocal } from "@app/helpers/secretRotationsV2";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { Auth0ClientSecretRotationReviewFields } from "./Auth0ClientSecretRotationReviewFields";
import { AwsIamUserSecretRotationReviewFields } from "./AwsIamUserSecretRotationReviewFields";
import { SqlCredentialsRotationReviewFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationReviewFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields
};
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 AwsIamUserSecretRotationSecretsMappingFields = () => {
const { control } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.AwsIamUserSecret;
}
>();
const { rotationOption } = useSecretRotationV2Option(SecretRotation.AwsIamUserSecret);
const items = [
{
name: "Access Key ID",
input: (
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
value={value}
onChange={onChange}
placeholder={rotationOption?.template.secretsMapping.accessKeyId}
/>
</FormControl>
)}
control={control}
name="secretsMapping.accessKeyId"
/>
)
},
{
name: "Client Secret",
input: (
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
value={value}
onChange={onChange}
placeholder={rotationOption?.template.secretsMapping.secretAccessKey}
/>
</FormControl>
)}
control={control}
name="secretsMapping.secretAccessKey"
/>
)
}
];
return <SecretsMappingTable items={items} />;
};
@@ -4,12 +4,14 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { TSecretRotationV2Form } from "../schemas";
import { Auth0ClientSecretRotationSecretsMappingFields } from "./Auth0ClientSecretRotationSecretsMappingFields";
import { AwsIamUserSecretRotationSecretsMappingFields } from "./AwsIamUserSecretRotationSecretsMappingFields";
import { SqlCredentialsRotationSecretsMappingFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationSecretsMappingFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields
};
export const SecretRotationV2SecretsMappingFields = () => {
@@ -0,0 +1,18 @@
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";
export const AwsIamUserSecretRotationSchema = z
.object({
type: z.literal(SecretRotation.AwsIamUserSecret),
parameters: z.object({
clientName: z.string().trim().min(1, "Client Name required"),
region: z.string().trim().min(1, "Region required")
}),
secretsMapping: z.object({
accessKeyId: z.string().trim().min(1, "Access Key ID required"),
secretAccessKey: z.string().trim().min(1, "Secret Access Key required")
})
})
.merge(BaseSecretRotationSchema);
@@ -1,13 +1,15 @@
import { z } from "zod";
import { Auth0ClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/auth0-client-secret-rotation-schema";
import { AwsIamUserSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/aws-iam-user-secret-rotation-schema";
import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema";
import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema";
const SecretRotationUnionSchema = z.discriminatedUnion("type", [
PostgresCredentialsRotationSchema,
MsSqlCredentialsRotationSchema,
Auth0ClientSecretRotationSchema
Auth0ClientSecretRotationSchema,
AwsIamUserSecretRotationSchema
]);
export const SecretRotationV2FormSchema = SecretRotationUnionSchema;
+9 -2
View File
@@ -19,20 +19,27 @@ export const SECRET_ROTATION_MAP: Record<
name: "Auth0 Client Secret",
image: "Auth0.png",
size: 35
},
[SecretRotation.AwsIamUserSecret]: {
name: "AWS IAM User Secret",
image: "Amazon Web Services.png",
size: 35
}
};
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
};
// if a rotation can potentially have downtime due to rotating a single credential set this to false
export const IS_ROTATION_DUAL_CREDENTIALS: Record<SecretRotation, boolean> = {
[SecretRotation.PostgresCredentials]: true,
[SecretRotation.MsSqlCredentials]: true,
[SecretRotation.Auth0ClientSecret]: false
[SecretRotation.Auth0ClientSecret]: false,
[SecretRotation.AwsIamUserSecret]: true
};
export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => {
@@ -4,15 +4,20 @@ import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "@app/hooks/api/appConnections";
import {
TAwsConnectionIamUser,
TAwsConnectionKmsKey,
TAwsConnectionListIamUsersResponse,
TAwsConnectionListKmsKeysResponse,
TListAwsConnectionIamUsers,
TListAwsConnectionKmsKeys
} from "./types";
const awsConnectionKeys = {
all: [...appConnectionKeys.all, "aws"] as const,
listKmsKeys: (params: TListAwsConnectionKmsKeys) =>
[...awsConnectionKeys.all, "kms-keys", params] as const
[...awsConnectionKeys.all, "kms-keys", params] as const,
listIamUsers: (params: TListAwsConnectionIamUsers) =>
[...awsConnectionKeys.all, "iam-users", params] as const
};
export const useListAwsConnectionKmsKeys = (
@@ -40,3 +45,28 @@ export const useListAwsConnectionKmsKeys = (
...options
});
};
export const useListAwsConnectionIamUsers = (
{ connectionId }: TListAwsConnectionIamUsers,
options?: Omit<
UseQueryOptions<
TAwsConnectionIamUser[],
unknown,
TAwsConnectionIamUser[],
ReturnType<typeof awsConnectionKeys.listIamUsers>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: awsConnectionKeys.listIamUsers({ connectionId }),
queryFn: async () => {
const { data } = await apiRequest.get<TAwsConnectionListIamUsersResponse>(
`/api/v1/app-connections/aws/${connectionId}/users`
);
return data.iamUsers;
},
...options
});
};
@@ -14,3 +14,21 @@ export type TAwsConnectionKmsKey = {
export type TAwsConnectionListKmsKeysResponse = {
kmsKeys: TAwsConnectionKmsKey[];
};
export type TListAwsConnectionIamUsers = {
connectionId: string;
};
export type TAwsConnectionIamUser = {
arn: string;
UserName: string;
};
export type TAwsConnectionListIamUsersResponse = {
iamUsers: TAwsConnectionIamUser[];
};
export type TAwsIamUserSecret = {
arn: string;
UserName: string;
};
@@ -1,7 +1,8 @@
export enum SecretRotation {
PostgresCredentials = "postgres-credentials",
MsSqlCredentials = "mssql-credentials",
Auth0ClientSecret = "auth0-client-secret"
Auth0ClientSecret = "auth0-client-secret",
AwsIamUserSecret = "aws-iam-user-secret"
}
export enum SecretRotationStatus {
@@ -0,0 +1,38 @@
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 TAwsIamUserSecretRotation = TSecretRotationV2Base & {
type: SecretRotation.AwsIamUserSecret;
parameters: {
region: string;
clientName: string;
};
secretsMapping: {
accessKeyId: string;
secretAccessKey: string;
};
};
export type TAwsIamUserSecretRotationGeneratedCredentials = {
accessKeyId: string;
secretAccessKey: string;
};
export type TAwsIamUserSecretRotationGeneratedCredentialsResponse =
TSecretRotationV2GeneratedCredentialsResponseBase<
SecretRotation.AwsIamUserSecret,
TAwsIamUserSecretRotationGeneratedCredentials
>;
export type TAwsIamUserSecretRotationOption = {
name: string;
type: SecretRotation.AwsIamUserSecret;
connection: AppConnection.AWS;
template: {
secretsMapping: TAwsIamUserSecretRotation["secretsMapping"];
};
};
@@ -4,6 +4,11 @@ import {
TAuth0ClientSecretRotationGeneratedCredentialsResponse,
TAuth0ClientSecretRotationOption
} from "@app/hooks/api/secretRotationsV2/types/auth0-client-secret-rotation";
import {
TAwsIamUserSecretRotation,
TAwsIamUserSecretRotationGeneratedCredentialsResponse,
TAwsIamUserSecretRotationOption
} from "@app/hooks/api/secretRotationsV2/types/aws-iam-access-key-rotation";
import {
TMsSqlCredentialsRotation,
TMsSqlCredentialsRotationGeneratedCredentialsResponse
@@ -20,13 +25,15 @@ export type TSecretRotationV2 = (
| TPostgresCredentialsRotation
| TMsSqlCredentialsRotation
| TAuth0ClientSecretRotation
| TAwsIamUserSecretRotation
) & {
secrets: (SecretV3RawSanitized | null)[];
};
export type TSecretRotationV2Option =
| TSqlCredentialsRotationOption
| TAuth0ClientSecretRotationOption;
| TAuth0ClientSecretRotationOption
| TAwsIamUserSecretRotationOption;
export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] };
@@ -35,7 +42,8 @@ export type TSecretRotationV2Response = { secretRotation: TSecretRotationV2 };
export type TViewSecretRotationGeneratedCredentialsResponse =
| TPostgresCredentialsRotationGeneratedCredentialsResponse
| TMsSqlCredentialsRotationGeneratedCredentialsResponse
| TAuth0ClientSecretRotationGeneratedCredentialsResponse;
| TAuth0ClientSecretRotationGeneratedCredentialsResponse
| TAwsIamUserSecretRotationGeneratedCredentialsResponse;
export type TCreateSecretRotationV2DTO = DiscriminativePick<
TSecretRotationV2,
@@ -82,10 +90,12 @@ export type TSecretRotationOptionMap = {
[SecretRotation.PostgresCredentials]: TSqlCredentialsRotationOption;
[SecretRotation.MsSqlCredentials]: TSqlCredentialsRotationOption;
[SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationOption;
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption;
};
export type TSecretRotationGeneratedCredentialsResponseMap = {
[SecretRotation.PostgresCredentials]: TPostgresCredentialsRotationGeneratedCredentialsResponse;
[SecretRotation.MsSqlCredentials]: TMsSqlCredentialsRotationGeneratedCredentialsResponse;
[SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationGeneratedCredentialsResponse;
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse;
};