Add Azure Client Secrets Rotation

This commit is contained in:
carlosmonastyrski
2025-04-16 05:04:41 -03:00
parent 7d97a76ecc
commit 1184ea1b11
36 changed files with 861 additions and 36 deletions
@@ -0,0 +1,71 @@
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 { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
import { useAzureConnectionListClients } from "@app/hooks/api/appConnections/azure";
import { TAzureClient } from "@app/hooks/api/appConnections/azure/types";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
export const AzureClientSecretRotationParametersFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.AzureClientSecret;
}
>();
const connectionId = watch("connection.id");
const { data: clients, isPending: isClientsPending } = useAzureConnectionListClients(
connectionId,
{ enabled: Boolean(connectionId) }
);
return (
<Controller
name="parameters.appId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Application"
helperText={
<Tooltip
className="max-w-md"
content={
<>
Ensure that your connection has the{" "}
<span className="font-semibold">read_clients</span> permission and the application
exists in the connection&#39;s audience.
</>
}
>
<div>
<span>Don&#39;t see the application 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.id === value) ?? null}
onChange={(option) => {
onChange((option as SingleValue<TAzureClient>)?.id ?? null);
setValue("parameters.appName", (option as SingleValue<TAzureClient>)?.name ?? "");
}}
options={clients}
placeholder="Select an application..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
);
};
@@ -4,12 +4,14 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { TSecretRotationV2Form } from "../schemas";
import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRotationParametersFields";
import { AzureClientSecretRotationParametersFields } from "./AzureClientSecretRotationParametersFields";
import { SqlCredentialsRotationParametersFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields,
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields
};
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 AzureClientSecretRotationReviewFields = () => {
const { watch } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.AzureClientSecret;
}
>();
const [parameters, { clientId, clientSecret }] = watch(["parameters", "secretsMapping"]);
return (
<>
<SecretRotationReviewSection label="Parameters">
<GenericFieldLabel label="App Name">{parameters.appName}</GenericFieldLabel>
<GenericFieldLabel label="App ID">{parameters.appId}</GenericFieldLabel>
</SecretRotationReviewSection>
<SecretRotationReviewSection label="Secrets Mapping">
<GenericFieldLabel label="Client ID">{clientId}</GenericFieldLabel>
<GenericFieldLabel label="Client Secret">{clientSecret}</GenericFieldLabel>
</SecretRotationReviewSection>
</>
);
};
@@ -7,12 +7,14 @@ import { getRotateAtLocal } from "@app/helpers/secretRotationsV2";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { Auth0ClientSecretRotationReviewFields } from "./Auth0ClientSecretRotationReviewFields";
import { AzureClientSecretRotationReviewFields } from "./AzureClientSecretRotationReviewFields";
import { SqlCredentialsRotationReviewFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationReviewFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields,
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields
};
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 AzureClientSecretRotationSecretsMappingFields = () => {
const { control } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.AzureClientSecret;
}
>();
const { rotationOption } = useSecretRotationV2Option(SecretRotation.AzureClientSecret);
const items = [
{
name: "Client 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.clientId}
/>
</FormControl>
)}
control={control}
name="secretsMapping.clientId"
/>
)
},
{
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.clientSecret}
/>
</FormControl>
)}
control={control}
name="secretsMapping.clientSecret"
/>
)
}
];
return <SecretsMappingTable items={items} />;
};
@@ -4,12 +4,14 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { TSecretRotationV2Form } from "../schemas";
import { Auth0ClientSecretRotationSecretsMappingFields } from "./Auth0ClientSecretRotationSecretsMappingFields";
import { AzureClientSecretRotationSecretsMappingFields } from "./AzureClientSecretRotationSecretsMappingFields";
import { SqlCredentialsRotationSecretsMappingFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationSecretsMappingFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields,
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields
};
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 AzureClientSecretRotationSchema = z
.object({
type: z.literal(SecretRotation.AzureClientSecret),
parameters: z.object({
appId: z.string().trim().min(1, "App ID required"),
appName: z.string().trim().min(1, "App Name required")
}),
secretsMapping: z.object({
clientId: z.string().trim().min(1, "Client ID required"),
clientSecret: z.string().trim().min(1, "Client Secret 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 { AzureClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/azure-client-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,
AzureClientSecretRotationSchema
]);
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.AzureClientSecret]: {
name: "Azure Client Secret",
image: "Microsoft Azure.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.AzureClientSecret]: AppConnection.AzureClientSecrets
};
// 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.AzureClientSecret]: false
};
export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => {
@@ -0,0 +1 @@
export * from "./queries";
@@ -0,0 +1,37 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
import { TAzureClient } from "./types";
const azureConnectionKeys = {
all: [...appConnectionKeys.all, "azure"] as const,
listClients: (connectionId: string) =>
[...azureConnectionKeys.all, "clients", connectionId] as const
};
export const useAzureConnectionListClients = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TAzureClient[],
unknown,
TAzureClient[],
ReturnType<typeof azureConnectionKeys.listClients>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: azureConnectionKeys.listClients(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<{ clients: TAzureClient[] }>(
`/api/v1/app-connections/azure-client-secrets/${connectionId}/clients`
);
return data.clients;
},
...options
});
};
@@ -0,0 +1,4 @@
export type TAzureClient = {
name: string;
id: string;
};
@@ -1,7 +1,8 @@
export enum SecretRotation {
PostgresCredentials = "postgres-credentials",
MsSqlCredentials = "mssql-credentials",
Auth0ClientSecret = "auth0-client-secret"
Auth0ClientSecret = "auth0-client-secret",
AzureClientSecret = "azure-client-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 TAzureClientSecretRotation = TSecretRotationV2Base & {
type: SecretRotation.AzureClientSecret;
parameters: {
appId: string;
appName: string;
};
secretsMapping: {
clientId: string;
clientSecret: string;
};
};
export type TAzureClientSecretRotationGeneratedCredentials = {
clientId: string;
clientSecret: string;
};
export type TAzureClientSecretRotationGeneratedCredentialsResponse =
TSecretRotationV2GeneratedCredentialsResponseBase<
SecretRotation.AzureClientSecret,
TAzureClientSecretRotationGeneratedCredentials
>;
export type TAzureClientSecretRotationOption = {
name: string;
type: SecretRotation.AzureClientSecret;
connection: AppConnection.AzureClientSecrets;
template: {
secretsMapping: TAzureClientSecretRotation["secretsMapping"];
};
};
@@ -4,6 +4,11 @@ import {
TAuth0ClientSecretRotationGeneratedCredentialsResponse,
TAuth0ClientSecretRotationOption
} from "@app/hooks/api/secretRotationsV2/types/auth0-client-secret-rotation";
import {
TAzureClientSecretRotation,
TAzureClientSecretRotationGeneratedCredentialsResponse,
TAzureClientSecretRotationOption
} from "@app/hooks/api/secretRotationsV2/types/azure-client-secret-rotation";
import {
TMsSqlCredentialsRotation,
TMsSqlCredentialsRotationGeneratedCredentialsResponse
@@ -20,13 +25,15 @@ export type TSecretRotationV2 = (
| TPostgresCredentialsRotation
| TMsSqlCredentialsRotation
| TAuth0ClientSecretRotation
| TAzureClientSecretRotation
) & {
secrets: (SecretV3RawSanitized | null)[];
};
export type TSecretRotationV2Option =
| TSqlCredentialsRotationOption
| TAuth0ClientSecretRotationOption;
| TAuth0ClientSecretRotationOption
| TAzureClientSecretRotationOption;
export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] };
@@ -35,7 +42,8 @@ export type TSecretRotationV2Response = { secretRotation: TSecretRotationV2 };
export type TViewSecretRotationGeneratedCredentialsResponse =
| TPostgresCredentialsRotationGeneratedCredentialsResponse
| TMsSqlCredentialsRotationGeneratedCredentialsResponse
| TAuth0ClientSecretRotationGeneratedCredentialsResponse;
| TAuth0ClientSecretRotationGeneratedCredentialsResponse
| TAzureClientSecretRotationGeneratedCredentialsResponse;
export type TCreateSecretRotationV2DTO = DiscriminativePick<
TSecretRotationV2,
@@ -82,10 +90,12 @@ export type TSecretRotationOptionMap = {
[SecretRotation.PostgresCredentials]: TSqlCredentialsRotationOption;
[SecretRotation.MsSqlCredentials]: TSqlCredentialsRotationOption;
[SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationOption;
[SecretRotation.AzureClientSecret]: TAzureClientSecretRotationOption;
};
export type TSecretRotationGeneratedCredentialsResponseMap = {
[SecretRotation.PostgresCredentials]: TPostgresCredentialsRotationGeneratedCredentialsResponse;
[SecretRotation.MsSqlCredentials]: TMsSqlCredentialsRotationGeneratedCredentialsResponse;
[SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationGeneratedCredentialsResponse;
[SecretRotation.AzureClientSecret]: TAzureClientSecretRotationGeneratedCredentialsResponse;
};